【Android】使用 Compose 自定义 View 实现从 0 ~ 1 仿 EChat 柱状图

目录

  • 前言
  • DrawScope
    • DrawScope Api
  • 绘制柱状图
    • 绘制 X 轴
    • 绘制 Y 轴
    • 绘制柱状背景
    • 绘制柱状前景
    • 完整代码
    • 最终效果
  • 存在的问题

前言

本文讲的是使用 compose 去自定义 View ,如果您未曾通过继承 View 的方式去实现自定义 View,那么,我建议在观看本文之前呢,您先去了解一下如何通过继承 View 的方式实现自定义 View ,具体文章可阅读 👉 【Android】自定义View组件,并实现在 Compose、Kotlin、Xml 中调用

DrawScope

看到这里时,相信你已经学习了如何通过继承 View ,实现 onDraw 函数,并使用函数提供的 Canvas 实现自定义。而现在,我很遗憾的告诉你,在 Compose 的自定义 View 上,已经不再使用 Canvas 了。

如你所见,它只是嵌套了一层叫 Canvas 的皮:

在这里插入图片描述
真正的绘制方法,还得是 DrawScope,这下明白为啥文章要起个 DrawScope 的目录了吧?

DrawScope Api

使用 Compose 自定义绘制一个柱状图,并不难,稍微有点难度的是其中的逻辑,你看吧,关于 DrawScopeApi 我也才只用了以下三个而已。

当然啦,DrawScopeApi 可不只有这一点,点击查看全部 Api 👉 DrawScope

Api用途
drawText绘制TextMeasurer生成的现有文本布局。
drawLine 使用给定的油漆在给定点之间绘制一条线。
drawRoundRect使用给定的Paint绘制一个圆角矩形。矩形是填充还是描边(或两者)由Paint.style控制。

绘制柱状图

绘制 X 轴

fun drawTextX() {val barHeight = ((value.data / maxDataValue) * drawHeight)val xOffset = (index + 1) * (barWidth + ColumnarDistance.SPACE_BETWEEN_BARS.distance)val yOffset = (size.height - ColumnarDistance.BOTTOM.distance) - barHeightdrawScope.drawText(textMeasurer = textMeasurer,text = value.name,// 在左上方开始绘制的起点topLeft = Offset(xOffset, yOffset + barHeight),style = TextStyle(color = Color.Gray, fontSize = 12.sp))
}

绘制 Y 轴

fun drawTextY() {val size = drawScope.sizeval textMax = data.maxOf { it.data }val yAxisHeight = (size.height - ColumnarDistance.TOP.distance - ColumnarDistance.BOTTOM.distance) * percentagedrawScope.drawText(textMeasurer,(textMax * (1 - percentage)).toInt().toString(),style = TextStyle(color = Color.Gray, fontSize = 12.sp),topLeft = Offset(0f, yAxisHeight + 15))
}
fun drawLineY() {drawScope.drawLine(color = if (listY[index] == 1f) Color.Black else Color.Gray,// 绘制线的起点Offset(ColumnarDistance.LEFT_Y_X.distance,yAxisHeight + ColumnarDistance.TOP.distance),// 绘制线的终点Offset(size.width,yAxisHeight + ColumnarDistance.TOP.distance),strokeWidth = 1f)
}

绘制柱状背景

fun drawColumnarBg(barbackground: Color) {val barWidth = (size.width - (ColumnarDistance.SPACE_BETWEEN_BARS.distance * dataSize) - ColumnarDistance.RIGHT.distance) / dataSizeval barHeight = ((value.data / maxDataValue) * drawHeight)val xOffset = (index + 1) * (barWidth + ColumnarDistance.SPACE_BETWEEN_BARS.distance)drawScope.drawRoundRect(// 设置柱状的背景色color = barbackground,// 在左上方开始绘制的起点topLeft = Offset(xOffset,ColumnarDistance.TOP.distance),// 从 topLeft 绘制的起点开始算起,应该绘制多高、多宽的矩形size = Size(barWidth, drawHeight))
}

绘制柱状前景

fun drawColumnar() {val barWidth = (size.width - (ColumnarDistance.SPACE_BETWEEN_BARS.distance * dataSize) - ColumnarDistance.RIGHT.distance) / dataSizeval barHeight = ((value.data / maxDataValue) * drawHeight)val xOffset = (index + 1) * (barWidth + ColumnarDistance.SPACE_BETWEEN_BARS.distance)val yOffset = (size.height - ColumnarDistance.BOTTOM.distance) - barHeightdrawScope.drawRoundRect(// 设置柱状的背景色color = barColors,// 在左上方开始绘制的起点topLeft = Offset(xOffset,yOffset),// 从 topLeft 绘制的起点开始算起,应该绘制多高、多宽的矩形size = Size(barWidth, barHeight))
}

完整代码

@Composable
fun Histogram(data: List<HistogramData>, modifier: Modifier = Modifier
) {Box(modifier = Modifier.wrapContentSize().padding(16.dp),) {BarChart(data,modifier = modifier.width(400.dp).height(400.dp))}
}@Composable
internal fun BarChart(data: List<HistogramData>,modifier: Modifier = Modifier,barColors: Color = colorResource(R.color.columnar),barBackground: Color = colorResource(R.color.columnar_background)
) {val textMeasurer = rememberTextMeasurer()Canvas(modifier) {// 画柱状、x 轴的文字drawColumnar(this, data, barColors, barBackground, textMeasurer)// 画 y 轴drawYAxis(this, textMeasurer, data)}
}/*** 绘制柱状*/
internal fun drawColumnar(drawScope: DrawScope,data: List<HistogramData>,barColors: Color,barbackground: Color,textMeasurer: TextMeasurer
) {val maxDataValue = data.maxOf { it.data }val size = drawScope.sizeval dataSize = data.size + 1val barWidth =(size.width - (ColumnarDistance.SPACE_BETWEEN_BARS.distance * dataSize) - ColumnarDistance.RIGHT.distance) / dataSizedata.forEachIndexed { index, value ->val drawHeight =size.height - ColumnarDistance.BOTTOM.distance - ColumnarDistance.TOP.distanceval barHeight = ((value.data / maxDataValue) * drawHeight)val xOffset = (index + 1) * (barWidth + ColumnarDistance.SPACE_BETWEEN_BARS.distance)val yOffset = (size.height - ColumnarDistance.BOTTOM.distance) - barHeight// 绘制柱状的背景drawScope.drawRoundRect(// 设置柱状的背景色color = barbackground,// 在左上方开始绘制的起点topLeft = Offset(xOffset,ColumnarDistance.TOP.distance),// 从 topLeft 绘制的起点开始算起,应该绘制多高、多宽的矩形size = Size(barWidth, drawHeight))// 绘制柱状drawScope.drawRoundRect(// 设置柱状的背景色color = barColors,// 在左上方开始绘制的起点topLeft = Offset(xOffset,yOffset),// 从 topLeft 绘制的起点开始算起,应该绘制多高、多宽的矩形size = Size(barWidth, barHeight))// 绘制柱状下边的文字drawScope.drawText(textMeasurer,value.name,// 在左上方开始绘制的起点topLeft = Offset(xOffset, yOffset + barHeight),style = TextStyle(color = Color.Gray, fontSize = 12.sp))}
}internal fun drawYAxis(drawScope: DrawScope,textMeasurer: TextMeasurer,data: List<HistogramData>
) {val listY = listOf(0f, 0.25f, 0.5f, 0.75f, 1f)val size = drawScope.sizeval textMax = data.maxOf { it.data }listY.forEachIndexed { index, percentage ->val yAxisHeight =(size.height - ColumnarDistance.TOP.distance - ColumnarDistance.BOTTOM.distance) * percentage// 绘制 y 轴文字drawScope.drawText(textMeasurer,(textMax * (1 - percentage)).toInt().toString(),style = TextStyle(color = Color.Gray, fontSize = 12.sp),topLeft = Offset(0f,yAxisHeight + 15))// 绘制 y 轴横线drawScope.drawLine(color = if (listY[index] == 1f) Color.Black else Color.Gray,// 绘制线的起点Offset(ColumnarDistance.LEFT_Y_X.distance,yAxisHeight + ColumnarDistance.TOP.distance),// 绘制线的终点Offset(size.width,yAxisHeight + ColumnarDistance.TOP.distance),strokeWidth = 1f)}
}

最终效果

在这里插入图片描述

存在的问题

在写本文的规划时,原本是想着仿 echarts 官网的柱状图来开发一个 Compose 代码版本的柱状图,搞到最后才发现,柱的前景在初始化的时候无法实现从下往上绘制的动画,如下:
在这里插入图片描述
Compose 中,对于动画的操作只能在 @Composable 函数里面进行,但 DrawScope 并不是一个 @Composable 函数,这也就意味着我们无法将动画设置在 Canvas 里。

在这里插入图片描述
可能有人会产生一些小问题,比如说:
1、Canvas 里面无法使用 @Composable 函数,那我把他移动到 Canvas 外面来不可以吗?
答:可以。但是柱状图绘制的过程中是需要使用遍历的方式来完成多条数据的绘制,每一条数据都需要根据遍历的列表的数值来得到应该为柱的前景设置多少的高度,也就意味着数组每遍历一次,就要更新一下 barHeight 的数值,barHeight 使用了 remember 接收值,就会导致重组,当前的 @Composable 就会被执行一次刷新,刷新后又给 barHeight 赋值,接着又触发重组…

不用 remember 吧,你更新了 barHeight 又不会触发 animateFloatAsState 的变化。

@Composable
fun draw() {var barHeight by remember { mutableStateOf(0f) }// 通过 animateFloatAsState 创建一个动画状态val animatedBarHeight by animateFloatAsState(targetValue = barHeight,animationSpec = androidx.compose.animation.core.tween(durationMillis = 1000))Canvas(modifier) {data.forEachIndexed { index, value ->// 计算柱需要的高度barHeight = ((value.data / maxDataValue) * drawHeight)drawScope.drawRoundRect(color = barColors,topLeft = Offset(xOffset, yOffset),size = Size(barWidth, animatedBarHeight)}}
}

2、data.forEachIndexed 使用 LaunchedEffect ?
答:不行。LaunchedEffect 也是一个 @Composable 函数,@Composable 只能在 @Composable 函数里面使用。

针对这一问题,或许可以使用自定义 Layout 的方式实现,即将柱状的前景和背景作为子组件实现…

源码地址 👉 GitCode

参考文档

1、Android Developers - DrawScope 参考文档
2、Android Developers - Compose 中的图形
3、学不动也要学,Jetpack Compose 实现自定义绘制

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

监控-08-skywalking监控告警

文章目录 前言一、准备二、配置skywalking2.1 修改alarm-settings.yml2.2 重启skywalking 三、收到告警邮件总结 前言 skywalking根据监控规则&#xff0c;通过webhook调后端微服务接口&#xff0c;从而发送告警邮件。 一、准备 根据上几章内容&#xff0c;保证skywalking能监…

离散数学实验二c语言(输出关系矩阵,输出矩阵性质,输出自反闭包,对称闭包,传递闭包,判断矩阵是否为等价关系,相容关系,偏序关系)

离散数学实验二 一、算法描述&#xff0c;算法思想 &#xff08;一&#xff09;相关数据结构 typedef struct Set *S; //存放集合 struct Set {int size; //集合的元素个数char *A; //存放该集合的元素 }; Set存放有限集合A&#xff0c;该集合的元素个数为size&#xff0…

Kafka-Windows搭建全流程(环境,安装包,编译,消费案例,远程连接,服务自启,可视化工具)

目录 一. Kafka安装包获取 1. 官网地址 2. 百度网盘链接 二. 环境要求 1. Java 运行环境 (1) 对 java 环境变量进行配置 (2) 下载完毕之后进行解压 三. 启动Zookeeper 四. 启动Kafka (1) 修改Conf下的server.properties文件&#xff0c;修改kafka的日志文件路径 (2)…

海外云手机实现高效的海外社交媒体营销

随着全球化的深入发展&#xff0c;越来越多的中国企业走向国际市场&#xff0c;尤其是B2B外贸企业&#xff0c;海外社交媒体营销已成为其扩大市场的重要手段。在复杂多变的海外市场环境中&#xff0c;如何有效提高营销效率并降低运营风险&#xff0c;成为了众多企业的首要任务。…

无人机悬停精度算法!

一、主要算法类型 PID控制算法&#xff1a; PID控制算法是一种常用的闭环控制算法&#xff0c;通过计算目标值与当前值的误差&#xff0c;并根据比例&#xff08;P&#xff09;、积分&#xff08;I&#xff09;、微分&#xff08;D&#xff09;三个参数来调整控制输出&#x…

SQL高级查询03

SQL查询语句的下载脚本链接&#xff01;&#xff01;&#xff01; 【免费】SQL练习资源-具体练习操作可以查看我发布的文章资源-CSDN文库​编辑https://download.csdn.net/download/Z0412_J0103/89908378https://download.csdn.net/download/Z0412_J0103/89908378 1 查询employ…

聚链成网,趣链科技参与 “跨链创新联合体”建设

近日&#xff0c;2024全球数商大会在上海举办。大会由上海数据集团和上海市数商协会联合主办&#xff0c;上海市数据局和浦东新区人民政府支持&#xff0c;以“数联全球&#xff0c;商通未来——‘链’接数字经济新未来”为主题&#xff0c;聚焦区块链技术和应用场景展开。 会上…

PostGis空间(下):空间连接与空间索引

目录 1、简介2、空间连接3、空间索引3.1 索引操作3.2 空间索引的工作原理3.2.1 R-Tree 3.3 空间索引函数3.4 仅索引查询3.5 ANALYZE3.6 VACUUMing3.7 函数列表 PS 1024到啦&#xff01;&#xff01;&#xff01; 先祝各位程序员或者想成为程序员正在奋斗中的伙伴1024程序员节快…

JavaScript进阶:手写代码挑战(二)

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;JavaScript篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript手写代码篇 在现代Web开发中&#xff0c;JavaScript 是不可或缺的编程语言…

Linux系统基础-进程间通信(5)_模拟实现命名管道和共享内存

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Linux系统基础-进程间通信(5)_模拟实现命名管道和共享内存 收录于专栏[Linux学习] 本专栏旨在分享学习Linux的一点学习笔记&#xff0c;欢迎大家在评论区交流讨…

Mac 使用 zsh 终端提示 zsh: killed 的问题

我的脚本的内容为&#xff1a; #!/bin/bashset -epids$(ps -ef | grep consul | grep -v grep | awk {print $2})for pid in $pids; doecho "kill process: $pid"kill -9 $pid donecd $(dirname $0)nohup ./consul agent -dev > nohup.log &可以看到这是一个…

阿里云项目启动OOM问题解决

问题描述 随着项目业务的增长&#xff0c;系统启动时内存紧张&#xff0c;每次第一次启动的时候就会出现oom第二次或者第n的时候&#xff0c;就启动成功了。 带着这个疑问&#xff0c;我就在阿里云上提交了工单&#xff0c;咨询为什么第一次提交失败但是后面却能提交成功尼&a…

Matlab学习01-矩阵

目录 一&#xff0c;矩阵的创建 1&#xff0c;直接输入法创建矩阵 2&#xff0c;利用M文件创建矩阵 3&#xff0c;利用其它文本编辑器创建矩阵 二&#xff0c;矩阵的拼接 1&#xff0c;基本拼接 1&#xff09; 水平方向的拼接 2&#xff09;垂直方向的拼接 3&#xf…

shell脚本-函数

文章目录 一、函数介绍什么是函数、为什么使用函数、如何使用函数 二、shell脚本中如何定义函数Way1Way2Way3 三、shell脚本中如何调用函数四、shell脚本中使用内置变量(1、#、?、2等等)五、函数的返回值、shell脚本中函数的返回值函数的返回值概念shell脚本中函数的返回值ret…

梦金园三闯港交所上市:年营收200亿元,靠加盟模式取胜

近日&#xff0c;梦金园黄金珠宝集团股份有限公司&#xff08;以下简称“梦金园”&#xff09;向港交所递交IPO申请&#xff0c;中信证券为其独家保荐人。贝多财经了解到&#xff0c;这已经是梦金园第三次向港股发起冲击&#xff0c;此前曾于2023年9月、2024年4月两度递表。 继…

刷题 - 图论

1 | bfs/dfs | 网格染色 200. 岛屿数量 访问到马上就染色&#xff08;将visited标为 true)auto [cur_x, cur_y] que.front(); 结构化绑定&#xff08;C17&#xff09;也可以不使用 visited数组&#xff0c;直接修改原始数组时间复杂度: O(n * m)&#xff0c;最多将 visited 数…

Deepinteraction 深度交互:通过模态交互的3D对象检测

一.前提 为什么要采用跨模态的信息融合? 点云在低分辨率下提供必要的定位和几何信息&#xff0c;而图像在高分辨率下提供丰富的外观信息。 -->因此必须采用跨模态的信息融合 提出的原因? 传统的融合办法可能会由于信息融合到统一表示中的不太完美而丢失很大一部分特定…

磁珠的工作原理:【图文讲解】

1&#xff1a;什么是磁珠 磁珠是一种被动组件&#xff0c;用来抑制电路中的高频噪声。磁珠是一种特别的扼流圈&#xff0c;其成分多半为铁氧体&#xff0c;利用其高频电流产生的热耗散来抑制高频噪声。磁珠有时也称为磁环、EMI滤波器、铁芯等。 磁珠是滤波常用的器件&#xf…

SpringMVC常用注解

RequestMapping接口的映射&#xff0c;可以将HTTP请求映射到控制器方法上&#xff0c;通过这个注解使用不同的映射&#xff0c;就可以区分不同的控制器&#xff0c;其中RequestMapping中还有不同的属性&#xff0c;比如method&#xff0c;params&#xff0c;produces等在这里我…

快速搭建SpringBoot3+Prometheus+Grafana

快速搭建SpringBoot3PrometheusGrafana 一、搭建SpringBoot项目 1.1 创建SpringBoot项目 1.2 修改pom文件配置 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://…