LookaheadLayout
is all about measuring and layout in Compose. You might remember this tweet from Doris Liu where she introduced it and shared a cool animation showing some examples of shared element transitions:
Sadly we cannot experience the animations in a static book, but those elements were animated when switching between two different screen states using the radio buttons from above. Here is how they look after transitioning to the double column style. The black rectangles represent the animation targets for size and position for each one of the shared elements.
Let's go over an example with actual code. Imagine a SmartBox
Composable that is able to switch between Row
and Column
layouts based on a mutable state that toggles on click:
@Composable
fun SmartBox() {
var vertical by remember { mutableStateOf(false) }
Box(Modifier.clickable { vertical = !vertical }) {
if (vertical) {
Column {
Text("Text 1")
Text("Text 2")
}
} else {
Row {
Text("Text 1")
Text("Text 2")
}
}
}
}
Ideally, both texts would be shared elements between the two states, since they are exactly the same. It would also be great to have an easy way to animate them (they change instantly now). We might also want to use movableContentOf to allow them to be reused and not lose their state during/after the animation.
A more ambitious version of the example from above would be to navigate between two Composable screens in an app based on a mutable state. The following example is a bit naive, since we would probably have more than just two screens in our app, but it will work for what is worth here:
@Composable
fun TvShowApp() {
var listScreen by remember { mutableStateOf(true) }
Box(Modifier.clickable { listScreen = !listScreen }) {
if (listScreen) {
CharacterList()
} else {
Detail()
}
}
}
These two screens could also contain some shared elements, so those are animated when we clicked on a character row within the CharacterList
. Maybe the character image, or the whole character row. In this scenario, each sub-tree would likely be more complex than a couple texts, since it represents a complete screen. If we wanted to animate the transition, we would need to set some magic numbers as animation targets, only because we happen to know the final position and size of all the shared elements in the destination screen. But this is not great. Ideally, Compose UI should be able to pre-calculate and give us that information beforehand, so we could use it to set our animations targets. Here is where LookaheadLayout
comes into play.
LookaheadLayout
is able pre-calculate the new measure and placement of its direct or indirect children when they change. This gives each child the ability to observe the pre-calculated values from its measure/layout pass, and use those to reize/reposition itself in order to gradually change over time (which creates the animation effect). In the shared element transition from the example above, each shared element would observe what would be its final size and position in the screen it is transitioning to, and use those values to animate itself. Another example can be a morph animation.
Yet another way of pre-calculating layouts
If we step back for a second, we could think of LookaheadLayout
as yet another way of pre-calculating layouts in Jetpack Compose, along with SubcomposeLayout
(subcomposition) and intrinsics. That said, there are important differences between the three that would be good to clear up front.
SubcomposeLayout: Delays composition until measure time, so we can use the available space to determine what nodes/subtrees to build. It is more about conditional composition than pre-layout. It is also quite expensive, so I would not recommend using it for layout pre-calculation in any other scenario, since it is much more than a measure/layout pass.
Intrinsics: They are quite more efficient than subcomposition, and work very similarly to
LookaheadLayout
internally. Both approaches invoke the measure lamdbas provided by the user inLayoutModifier
s orMeasurePolicy
s with different constraints in the same frame. But in the case of intrinsics, they are more of a tentative calculation in order to perform real measuring using the obtained values. Imagine a row with 3 children. In order to make its height match the height of the tallest child, it would need to get the intrinsic measures of all its children, and finally measure itself using the maximum one.LookaheadLayout: Used for precise pre-calculation of size and position of any (direct or indirect) child in order to power automatic animations. On top of measuring,
LookaheadLayout
also does a placement calculation based on the lookahead sizes.LookaheadLayout
also does a more aggressive caching than intrinsics to avoid looking ahead unless the tree has changed (e.g: new layout nodes, modifier change, etc). To achieve this,LookaheadDelegate
caches the lookahead measurement and placement in order to skip unnecessary re-calculations. Another difference with intrinsics is that there is an implicit guarantee inLookaheadLayout
that the layout will finally arrive at the state calculated by the lookahead, so it does not permit users to manipulate the lookahead constraints.
How it works
In practice, LookaheadLayout
performs a lookahead pass of measure and layout before the "normal" measure/layout pass, so the latter can make use of the values pre-calculated during the former in order to update the node on every frame. This lookahead pass happens only when the tree changes or when layout changes as a result of a State
change.
When the lookahead pass takes place, layout animations are bypassed, so measure and layout are performed as if animations would be finished already. In the future, all the layout animation apis will get updated to be automatically skipped during lookahead passes, by design. This should work for any direct or indirect child of the LookaheadLayout
.
To expose the pre-calculated data, LookaheadLayout
executes its content
lambda in a LookaheadLayoutScope
, which gives access to a couple modifiers that its children can use:
Modifier.intermediateLayout
: Called whenever the layout with the modifier is remeasured. Gives access to the new lookahead (pre-calculated) size of the layout, and it is expected to produce an intermediate layout based on that (target) size.Modifier.onPlaced
: Called on any re-layout of the layout with the modifier. Allows the child to adjust its own placement based on its parent. It provides access to lookahead coordinates for both theLookaheadLayout
and the modifier itself (child ofLookaheadLayout
), which allows to calculate both the lookahead position and current position relative to the parent. These can be saved and used fromintermediateLayout
when generating the intermediate layout in order to animate the layout position.
These two modifiers run during the normal measure/layout pass, so they can use the pre-calculated info provided in their lambdas to resize/reposition the layout towards the target values. The expectation is that users build custom modifiers on top of these two in order to create custom animations. Here is an example of a custom modifier to animate the constraints used to measure its children based on the lookahead size (extracted from the official sources):
A size animation is created to animate the size values (width/height). The animation will be restarted every time the layout changes (see
snapshotFlow
).This animation runs within a
LaunchedEffect
in order to avoid running uncontrolled side effects in the measure/layout phase (when this modifier runs).Pre-calculated
lookaheadSize
is available in theintermediateLayout
lambda and set as thetargetSize
in order to trigger the animation when it changes.lookaheadSize
is used to measure children so they gradually change their size. This is done by creating new fixed constraints that follow the size animation value on every frame, which creates the ultimate animation effect over time.
The lambda from
intermediateLayout
is skipped during the lookahead pass, since this modifier is used to produce intermediate states towards the lookahead one.
Once we have the custom modifier to animate constraints, we can use it from any LookaheadLayout
. Here is an example, also extracted from the official sources:
Mutable state is used to toggle the
Row
between "full width" and "short width".The
Row
layout changes are animated using theanimateConstraints
custom modifier from the previous snippet. Each time the mutable state is toggled, theRow
width will change and that will trigger a new lookahead pre-calculation (and therefore also the animation).Max width and max height among all the children are used to measure the
LookaheadLayout
so all of them can fit inside.All the children are placed in 0,0.
And this produces a nice automatic animation any time the layout changes. This has been a good example of a resize animation based on the pre-calculated lookahead size. Now, what about the lookahead position?
As we mentioned earlier, there is also a Modifier.onPlaced
available in the LookaheadLayoutScope
. The onPlaced
modifier exists for adjusting the placement (position) of the child based on its parent. It provides enough data to calculate both the lookahead position and current position of the layout relative to the parent. Then we can save those in variables and use them on subsequent calls to intermediateLayout
, so we can also re-position the layout accordingly towards the lookahead values.
Here is an example of it, also extracted from the official sources. It animates the local position of the modified layout whenever the layout changes:
An offset animation is used to animate the position. It will animate the offset relative to the parent.
This animation runs within a
LaunchedEffect
once again to avoid running uncontrolled side effects in the measure/layout phase (when this modifier runs).lookaheadScopeCoordinates
andlayoutCoordinates
are available in theonPlaced
lambda. The former corresponds to the lookahead coordinates of theLookaheadLayout
itself, and the latter provides the lookahead coordinates of this child layout. Those values are used to calculate the current and target offsets in local coordinates.The calculated offsets are ultimately used from
intermediateLayout
to update the layout position accordingly (seeplaceable.place(x, y)
).
Once we have the custom modifier ready, we can use it for any (direct or indirect) children of a LookaheadLayout
, exactly the same way as we did in the previous examples.
Internals of Lookaheadlayout
Now that we have a sense on what LookaheadLayout
is and how it works, we are ready to learn about its internals. And the best way to do it is with some diagrams.
The diagrams we are going to show in this section have been facilitated by Doris Liu from the Google Jetpack Compose team. They show what happens during the initial measure and layout passes, including lookahead measure and lookahead layout (in orange).
Let's start with the measure pass.
When a LayoutNode
needs to be measured for the first time (e.g: it has just been attached) it will check if it is placed at the root of a LookaheadLayout
, in order to start the lookahead measure pass first (the lookahead pass only runs for the LookaheadLayout
subtree).
The LayoutNode
calls LookaheadPassDelegate#measure()
to start the lookahead measure pass. This delegate takes care of all the incoming lookahead measure/layout requests for the node. This call uses the outer LayoutNodeWrapper
(normally used to measure the node) to run its lookahead measuring, via its LookaheadDelegate
.
Looking back to the graphics about measuring in Compose, where we learned about the chain of LayoutNodeWrapper
s, we will remember that modifiers are also wrapped and chained, so the outer wrapper measures the current node, and wraps the first modifier wrapper, which wraps the second modifier wrapper, and so on. The last modifier in the chain wraps the inner LayoutNodeWrapper
, which is used to measure children (via their outer wrapper). This is also what happens here, but lookahead measure is executed on all the steps via their LookaheadDelegate
, instead of performing a "normal" measure.
Once the lookahead measure is done for the root node and all its direct or indirect children, or if the node is not at the root of the LookaheadLayout
, the "normal" measure pass runs.
The process of measuring basically follows what we have described a second ago. The only difference is that now, the MeasurePassDelegate
is used in all the steps instead of the lookahead one, since it is time to perform real measuring.
Let's give a look to the layout pass now.
The layout pass is not different at all. It is exactly the same we have already described for the measure pass, and the same delegates are used. The only difference is that in this case, placeAt(...)
is called, in order to place the node and its children (for normal layout pass), or in order to calculate its lookahead position (for lookahead layout pass, in orange).
So far we have focused on nodes that are measured/laid out for the first time. In the other hand, when a LayoutNode
needs a remeasure/relayout (e.g: its content has changed), the timming is a bit different. The invalidation for measure/layout and lookahead is optimized to reduce invalidation scope as much as possible. That way, LayoutNode
s that aren't affected by a change in the tree will not be invalidated. This makes it entirely possible that only a (small) portion of the LookaheadLayout
subtree gets invalidated due to changes to the tree.
Some extra bits
When a new LayoutNode
is attached, it inherits the out-most existing LookaheadScope
from the tree, so all the direct or indirect children of a LookaheadLayout
can share the same scope. This is because nested LookaheadLayout
s are supported. Compose UI also ensures that a single lookahead pass for all of them.
LookaheadLayout
can be combined with movableContentOf
and movableContentWithReceiverOf
in order to keep the state of the Composables while they are animated.
LookAheadLayout
will be released in Compose 1.3.
👨🏫 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.
Share this and consider a paid subscription
If you found this post useful and you are a free subscriber, please consider converting to a paid subscription for supporting this newsletter. All the help will be really appreciated and will allow me to put more time on it and write about more interesting stuff. If you are a paid subscriber already, thanks so much for your support 🙏
Paid subscription will give you access to additional content that free subscribers will not be able to read.
Jorge.