Foreword
Google released the Compose Material3 1.2.0-alpha04 version on July 28. In this version, two new components were added (modified), vertical separators and segmented buttons:
Experimental Segmented Button API.
Dividers now have a parameter to control orientation to support vertical dividers.
This article will analyze the source code of the separator and explain a strange place I found when looking at the source code.
Text
Update content
Before we start, let me tell you a little story.
On the Android developer website, Google’s update record gives a different API for this new component than the final API…
Regarding the change of the separator, what is said in the change log is that a parameter has been added to Divider
to specify whether the separator is a vertical separator. The same is true for the code in the commit record attached to the update record. written:
But when I updated my MD3 to this version, I found that Divider
did not have the horizontal
parameter. Instead, Divider
was abandoned, and then Two new components have been added: VerticalDivider()
and HorizontalDivider()
. The former is the actual new vertical separator, and the latter is actually the Divider before the update.
.
I thought I wrote the wrong version number, but I looked left and right and found that it was indeed correct. Then I checked compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider Modification history of .kt
files:
Oh, the co-authorship has been changed again. It turns out that Google does the same thing, so it’s okay, hahaha.
Source code analysis
Before this Divider
update, if we wanted to use Compose to display a vertical divider, we would usually write like this:
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) @Composable fun PreviewTest() {<!-- --> Row( modifier = Modifier.height(IntrinsicSize.Min) ) {<!-- --> Text(text = "text1") Divider( modifier = Modifier.fillMaxHeight().width(1.dp) ) Text(text = "text2") } }
display effect:
What this code does is set the height of the Divider
to fill the maximum height, and then set the width to 1 dp, which is the width of the divider.
In fact, as long as we look at the source code of Divider
, we will find that using it this way is a bit “unnecessary”:
@Composable fun Divider( modifier: Modifier = Modifier, thickness: Dp = DividerDefaults.Thickness, color: Color = DividerDefaults.color, ) {<!-- --> //… Box( modifier .fillMaxWidth() .height(targetThickness) .background(color = color) ) }
The implementation of Divider
only defines a Box
that fills the entire width, has a height of the border width set by the parameter, and a background color.
So if we need vertical separators, we only need to simply change the official source code:
@Composable fun Divider( modifier: Modifier = Modifier, thickness: Dp = DividerDefaults.Thickness, color: Color = DividerDefaults.color, ) {<!-- --> //… Box( modifier .fillMaxHeight() .width(targetThickness) .background(color = color) ) }
Hey, guess what? The first version of the officially implemented vertical separator actually does this:
It’s just that the final version was changed to another implementation method, and horizontal segmentation and vertical segmentation were split into two different components. Even so, the core principles are actually the same:
VerticalDivider:
@Composable funVerticalDivider( modifier: Modifier = Modifier, thickness: Dp = DividerDefaults.Thickness, color: Color = DividerDefaults.color, ) = Canvas(modifier.fillMaxHeight().width(thickness)) {<!-- --> drawLine( color = color, strokeWidth = thickness.toPx(), start = Offset(thickness.toPx() / 2, 0f), end = Offset(thickness.toPx() / 2, size.height), ) }
HorizontalDivider:
@Composable fun HorizontalDivider( modifier: Modifier = Modifier, thickness: Dp = DividerDefaults.Thickness, color: Color = DividerDefaults.color, ) = Canvas(modifier.fillMaxWidth().height(thickness)) {<!-- --> drawLine( color = color, strokeWidth = thickness.toPx(), start = Offset(0f, thickness.toPx() / 2), end = Offset(size.width, thickness.toPx() / 2), ) }
As you can see, the new implementation no longer uses Box
to implement it, but instead creates a Canvas
and uses drawLine
in it.
The size of Canvas
is the same as when using Box
above. If it is split horizontally, then modifier.fillMaxWidth().height(thickness)
, if If it is vertical division, then modifier.fillMaxHeight().width(thickness)
.
Next, there is the confusing code that makes me puzzled. I don’t know if my level is too low to understand or if the code here is really confusing.
Here we take HorizontalDivider
as an example. It draws a line in Canvas
with a width of thickness
, and the starting point is (0, half the line width). A line whose end point is (Canvas width, half line width).
Translated into human language, it uses drawLine
to draw a line and fill the entire Canvas
…
Forehead? I personally understand that replacing Box
with Canvas
is because Canvas
has better performance than Box
, because in fact Compose UI rendering eventually returns to “drawing” using Canvas
. Although the Canvas
here is different from Android’s Canvas and Compose’s Canvas, I still understand it this way. .
So, why bother using line drawing to fill this Canvas
?
Moreover, my above understanding is actually incorrect. Let’s take a look at the implementation of Canvas
:
@Composable fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) = Spacer(modifier.drawBehind(onDraw))
As you can see, this Canvas
actually creates a new Composable function of Spacer
, and then uses drawBehind
to receive the incoming DrawScope
.
In this case, wouldn’t it be enough to just use a simple Composable function to draw it?
If you don’t like Box
, it’s not impossible to use Spacer
:
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) @Composable fun PreviewTest() {<!-- --> Row( modifier = Modifier.height(IntrinsicSize.Min) ) {<!-- --> Text(text = "text1") Spacer(modifier = Modifier.fillMaxHeight().width(1.dp).background(Color.Gray)) Text(text = "text2") } }
So why did the implementation code here suddenly change to this? Puzzled.
So I continued to check the submission record of this change:
Oh, so it was to fix a bug?
let me see:
Okay, excuse me, this is not a public issue…
It seems that this trouble can only stay with me like this (
One More Thing
When using vertical separators, if the parent component is Surface
and the size is set to the maximum size:
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) @Composable fun PreviewTest() {<!-- --> Surface(modifier = Modifier.fillMaxSize()) {<!-- --> Row( modifier = Modifier.height(IntrinsicSize.Min) ) {<!-- --> Text(text = "Hello equationl!") VerticalDivider() Text(text = "Hello again!") } } }
Then, it will become like this:
The separator fills the entire height!
Obviously, from our interpretation of the source code above, we can know that the separator is implemented by setting fillMaxXXX
in the corresponding direction and then setting the other direction to the width of the separator.
Here, because our top-level component has fillMaxSize()
set, when we add a separator, it will naturally expand to fill the entire screen.
However, if you look at this code carefully, you will find that we actually set Row(modifier = Modifier.height(IntrinsicSize.Min))
in its parent component. So why does this separator still appear? What about filling the entire screen?
This is because of Modifier.height
:
Declare the preferred height of the content to be the same as the min or max intrinsic height of the content. The incoming measurement Constraints may override this value, forcing the content to be either smaller or larger.
In other words, the value we pass in here may be overwritten. If you want it not to be overwritten, you should use Modifier.requiredHeight
:
Declare the height of the content to be exactly the same as the min or max intrinsic height of the content. The incoming measurement Constraints will not override this value. If the content intrinsic height does not satisfy the incoming Constraints, the parent layout will be reported a size coerced in the Constraints, and the position of the content will be automatically offset to be centered on the space assigned to the child by the parent layout under the assumption that Constraints were respected.
Using this modifier, the passed value will not be overwritten:
Therefore, the Row
component will only use the minimum height, that is, the height of the separator will be consistent with the Text
at the same level.
Summary
The above is my source code analysis of the newly added delimiter in MD3 and a confusing problem I found when viewing the source code. But at present, I still don’t know why Google designed it this way. If anyone knows, I hope they can enlighten me.