前语

谷歌在 7 月 28 日发布了 Compose Material3 1.2.0-alpha04 版本,在该版本新增(修正)了两个组件,笔直分隔符和分段按钮:

Experimental Segmented Button API.

Dividers now have a parameter to control orientation to support vertical dividers.

本文将解析分隔符的源码并论述我在看源码时发现一个奇怪的当地。

正文

更新内容

在正式开端之前先说一个小插曲。

Android developer 网站上,谷歌的更新记载给出的这个新组件的 API 和最终发布的 API 不相同……

关于分隔符的变动,在更新日志中说的是为 Divider 添加了一个参数用于指定这个分隔符是否是笔直分隔符,更新记载附上的提交记载中的代码也是这样写的:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

可是当我更新我的 MD3 到 这个版本时,却发现 Divider 并没有 horizontal 这个参数,取而代之的是 Divider 被抛弃,然后新增了两个组件: VerticalDivider()HorizontalDivider() ,前者便是实践新增的笔直分隔符,而后者其实便是更新前的 Divider

我还认为我写错了版本号,可是我左看右看,确实没写错啊,后往来不断翻了一下 compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Divider.kt 文件的修正历史:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

哦,合着是又改了啊,本来谷歌也会这样啊,那没事了,啊哈哈哈。

源码解析

在本次 Divider 更新前,假如咱们想要运用 Compose 显现一个笔直的分隔线通常会这样写:

@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")
 }
}

显现效果:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

这段代码的意思是通过将 Divider 的高度设置为填充溢最大高度,然后将宽度设置为 1 个 dp,也便是分隔线的宽度来完成的。

其实咱们只要看一下 Divider 的源码,就会发现这样运用有点“多此一举”了:

@Composable
fun Divider(
    modifier: Modifier = Modifier,
    thickness: Dp = DividerDefaults.Thickness,
    color: Color = DividerDefaults.color,
) {
	// ……
    Box(
        modifier
            .fillMaxWidth()
            .height(targetThickness)
            .background(color = color)
    )
}

Divider 的完成也只是界说了一个填充溢全部宽度,而且高度为参数设置的边框宽度且带有背景色彩的 Box 算了。

所以假如咱们需要笔直分隔符的话,只需要简略改一下官方源码即可:

@Composable
fun Divider(
    modifier: Modifier = Modifier,
    thickness: Dp = DividerDefaults.Thickness,
    color: Color = DividerDefaults.color,
) {
    // ……
    Box(
        modifier
            .fillMaxHeight()
            .width(targetThickness)
            .background(color = color)
    )
}

欸,你猜怎么着?官方完成的笔直分隔符第一版还真是这样做的:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

只不过最终发布的版本改成了另外一种完成方法,而且还把水平切割和笔直切割拆分成了两个不同的组件,虽说如此,其实核心原理都差不多:

VerticalDivider:

@Composable
fun VerticalDivider(
    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),
    )
}

能够看到,新的完成方法不再选用 Box 来完成,而是改用创建一个 Canvas 并在其间 drawLine 来完成。

Canvas 的尺度则和上述运用 Box 时相同,假如是水平切割则 modifier.fillMaxWidth().height(thickness) ,假如是笔直切割则 modifier.fillMaxHeight().width(thickness)

接下来,便是让我百思不得其解的利诱代码,咱也不知道是我层次太低无法了解仍是这儿的代码确实有点利诱。

这儿以 HorizontalDivider 举例,它在 Canvas 中画了一条宽度为 thickness,起点为 (0, 线条宽度二分之一),终点为 (Canvas 宽度, 线条宽度二分之一) 的线。

翻译成人话便是,它用 drawLine 画了一条线把整个 Canvas 填充了……

额?把 Box 换成 Canvas 我个人了解为是因为 Canvas 性能比 Box 好,因为事实上 Compose 的 UI 烘托最终也是回归到运用 Canvas “画”出来的,虽然这儿的 Canvas 是 Android 的 Canvas 和 Compose 的 Canvas 不相同,可是我仍是如此去了解了。

那么,为什么还要多此一举的运用画线的方法去填充溢这个 Canvas 呢?

何况,实践上我上面的了解也是不正确的,咱们看一下这个 Canvas 的完成:

@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
    Spacer(modifier.drawBehind(onDraw))

能够看到,这个 Canvas 实践上也是新建了一个 Spacer 的 Composable 函数,然后在其间运用 drawBehind 接收了传入的 DrawScope

既然如此,那么直接运用一个简略的 Composable 函数来画不就行了?

不喜欢 Box 那就用 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")
 }
}

所以这儿的完成代码后来为什么忽然改成了这样呢?百思不得其解。

所以我又继续检查这次更改的提交记载:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

哦,本来是为了修复某个 bug ?

让我看看:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

好吧,打扰了,不是公开 issue ……

看来这个困扰只能这样一向随同我了(

One More Thing

在运用笔直切割符时,假如父组件是 Surface 且设置了尺度为最大尺度:

@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!")
        }
    }
}

那么,将会变成这样:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

分隔符会填充溢整个高度!

明显,通过上面咱们对源码的解读,咱们能够知道,切割符的完成便是通过在对应的方向上设置 fillMaxXXX 然后将另外一个方向设置为分隔符的宽度来完成的。

在这儿,因为咱们的顶层组件设置了 fillMaxSize(),所以当咱们添加分隔符时,它自然会被扩展到充溢整个屏幕。

可是,假如你仔细看这段代码,你会发现咱们其实在它的父组件中设置了 Row(modifier = Modifier.height(IntrinsicSize.Min)) 那为什么这个分隔符仍是会充溢整个屏幕呢?

这是因为 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.

也便是说,咱们这儿传入的值可能会被掩盖,假如想让它不被掩盖,应该运用 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.

运用这个修饰符,传入的值就不会被掩盖:

Compose Material3 新增垂直分隔符(VerticalDivider)解析与疑惑

然后 Row 组件会仅运用最小高度,也便是说,分隔符的高度将和其间同级的 Text 保持一致。

总结

以上便是我对 MD3 最新增加的分隔符的源码解析以及在检查源码时发现的一个令人利诱的问题。可是目前我仍是不知道为什么谷歌会这样设计,假如有大佬知道希望能不吝赐教。