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

news2025/3/15 4:23:35

前言

谷歌在 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 添加了一个参数用于指定这个分隔符是否是垂直分隔符,更新记录附上的提交记录中的代码也是这样写的:

1.png

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

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

2.png

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

源码解析

在本次 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")
 }
}

显示效果:

3.png

这段代码的意思是通过将 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)
    )
}

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

4.png

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

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

所以这里的实现代码后来为什么突然改成了这样呢?百思不得其解。

于是我又继续查看这次更改的提交记录:

5.png

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

让我看看:

6.png

好吧,打扰了,不是公开 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!")
        }
    }
}

那么,将会变成这样:

7.png

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

显然,通过上面我们对源码的解读,我们可以知道,分割符的实现就是通过在对应的方向上设置 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.

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

8.png

从而 Row 组件会仅使用最小高度,也就是说,分隔符的高度将和其中同级的 Text 保持一致。

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1103783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TwinCAT3 ADS与C++通讯

文章目录 一 ADS简介1.1 ADS通讯定义1.2 ADS通讯实现 二 上位机程序编写(Visual Studio 2019)2.1 启动VS2019,新建MFC项目2.2 添加ADS通讯链接库2.3 在程序中引入头文件 一 ADS简介 1.1 ADS通讯定义 ADS(Advanced Design System&#xff09…

从0开始编写BP,自适应学习率的BP神经网络,不使用MATLAB工具箱,纯手写matlab代码,以BP分类为例...

与上篇文章不同,仔细读了上篇文章的小伙伴应该知道,BP神经网络是有一个学习率的,而这个学习率很大程度上决定着神经网络的效果。这里采用自适应学习率,实现纯手写BP神经网络。 编程时,激活函数选择Sigmoid函数&#xf…

【计算机毕设选题推荐】网络在线考试系统SpringBoot+SSM+Vue

前言:我是IT源码社,从事计算机开发行业数年,专注Java领域,专业提供程序设计开发、源码分享、技术指导讲解、定制和毕业设计服务 项目名 网络在线考试系统 技术栈 SpringBootSSMVueMySQLMaven 文章目录 一、网络在线考试系统-环境…

SLAM从入门到精通(dwa算法)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 要说搜路算法,这个大家都比较好理解。毕竟从一个地点走到另外一个地点,这个都是直觉上可以感受到的事情。但是这条道路上机…

行情分析——加密货币市场大盘走势(10.17)

大饼昨日在受到假消息美国证券交易委员会(SEC)通过大饼ETF后迅速上涨,一度上涨到30000,而很快回落到28000附近。从MACD日线来看,现在完全进入多头趋势,同时大饼再次进入蓝色上涨趋势线,目前按照…

李宏毅机器学习笔记-半监督学习

半监督学习,一般应用于少量带标签的数据(数量R)和大量未带标签数据的场景(数量U),一般来说,U>>R。 半监督学习一般可以分为2种情况,一种是transductive learning,…

使用秋云 ucharts echarts 高性能跨全端图表组件 流程

1. 2. // 引入 import qiunDataCharts from ../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue // 注册 components:{qiunDataCharts },// 页面中使用 <qiun-data-charts type"line" :opts"opts" :chartData"…

04 MIT线性代数-矩阵的LU分解 Factorization into A=LU

目的: 从矩阵的角度理解高斯消元法, 完成LU分解得到ALU 1.矩阵乘积的逆矩阵 Inverse of a product 2.矩阵乘积的转置 Transpose of a product 3.转置矩阵的逆矩阵 Inverse of a transpose 4.矩阵的LU分解 U为上三角阵(Upper triangular matrix), L为下三角阵(Lower triangular…

pycharm远程连接miniconda完整过程,以及遇到的问题解决

问题1&#xff1a;no-zero exit code(126) env: ‘/home/user2/miniconda3/envs/ihan/bin/python3’: Too many levels of symbolic links Python interpreter process exited with a non-zero exit code 126 因为选择的新建导致太多软连接&#xff0c;先在服务器上建好虚拟环…

【微信小程序】数字化会议OA系统之首页搭建(附源码)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《微信小程序开发实战》。&#x1f3af;&#x1f3a…

eNSP笔记①

关闭范文信息&#xff1a;undo terminal monitor VRP三种试图 "<>"表示用户视图&#xff0c;系统默认的状态。主要用于查询设备基础信息或者状态等&#xff0c;也可以执行保存(save)。 “[]” 表示系统视图&#xff0c;在用户视图下输入system-view进入状态…

Kafka序列化反序列化解析、kafka schema

Kafka序列化反序列化解析、kafka schema。 kafka有自己的rpc协议,即nio bytebuf中的数据格式,详见之前的kafka相关介绍的文章。这里我们来看一下大家常用,有时又疑惑的序列化反序列化,对应rpc协议中的records,kafka叫Serdes,实际上也是字面上的意思serialize and deseri…

Qt扫盲-QDataStream 序列化和反序列化理论

QDataStream 序列化和反序列化理论 一、概述二、QDataStream 概述三、版本控制四、读取和写入原始二进制数据五、读写Qt集合类六、读写其他Qt类七、使用读事务八、Qt支持的序列化类型 一、概述 序列化&#xff1a; 指的是将一个内存对象转化成一串字节数据&#xff08;存储在一…

FSDP(Fully Sharded Data Parallel)

完全分片数据并行 (FSDP) &#xff0c;它将AI 模型的参数分片到数据并行工作器上&#xff0c;并且可以选择将部分训练计算卸载到 CPU。顾名思义&#xff0c;FSDP 是一种数据并行训练算法。尽管参数被分片到不同的GPU&#xff0c;但每个微批次数据的计算仍然是每个 GPU Worker 本…

C++ - 一些特殊类的设计

前言 我们在日常写项目的过程当中&#xff0c;肯定会遇到各种各样的需求&#xff0c;那么也就要求我们要写各种各样的类。本篇博客当中&#xff0c;就一些常用的特殊类进行介绍和实现。 不能被拷贝的类 关于实例化类拷贝&#xff08;对象的拷贝&#xff09;一般就是两个场景&…

6.DApp-用Web3实现前端与智能合约的交互

题记 用Web3实现前端与智能合约的交互&#xff0c;以下是操作流程和代码。 准备ganache环境 文章地址&#xff1a;4.DApp-MetaMask怎么连接本地Ganache-CSDN博客 准备智能合约 文章地址&#xff1a; 2.DApp-编写和运行solidity智能合约-CSDN博客 编写index.html文件 <!…

简单测试一下 展锐的 UDX710 性能

最近在接触 联通5G CPE VN007 &#xff0c;发现使用的是 展锐的Unisoc UDX710 CPU&#xff0c;正好简单的测试一下这颗CPU CPU信息 UDX710 是一颗 双核 ARM Cortex-A55 处理器&#xff0c;主频高达 1.35GHz processor : 0 BogoMIPS : 52.00 Features : fp…

ARM资源记录《AI嵌入式系统:算法优化与实现》第八章(暂时用不到)

1.CMSIS的代码 书里给的5&#xff0c;https://github.com/ARM-software/CMSIS_5 现在有6了&#xff0c;https://github.com/ARM-software/CMSIS_6 这是官网的书&#xff0c;介绍cmsis函数的https://arm-software.github.io/CMSIS_5/Core/html/index.html 2.CMSIS介绍 Cort…

Git命令在线练习网址--非常友好的提示及动画展示

Git命令在线练习 https://learngitbranching.js.org/ 举个栗子: 在练习时会给你相应提示,你可以按照相应步骤进行操作,并且每一步都有动画演示

A股风格因子看板 (2023.10 第05期)

该因子看板跟踪A股风格因子&#xff0c;该因子主要解释沪深两市的市场收益、刻画市场风格趋势的系列风格因子&#xff0c;用以分析市场风格切换、组合风格暴露等。 今日为该因子跟踪第05期&#xff0c;指数组合数据截止日2023-09-30&#xff0c;要点如下 近1年A股风格因子检验统…