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

news/2024/10/25 7:02:12/

目录

  • 前言
  • 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 轴

kotlin">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 轴

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

绘制柱状背景

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

绘制柱状前景

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

完整代码

kotlin">@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 的变化。

kotlin">@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.ppmy.cn/news/1541774.html

相关文章

3D虚拟服装试穿技术:迈向元宇宙与AR电商的新时代

随着电子商务的不断进化,消费者对于在线购物体验的需求也在不断提升。在这样的背景下,3D虚拟服装试穿技术正逐渐成为连接现实世界与数字世界的桥梁,为用户带来前所未有的沉浸式购物体验。本文将介绍一种创新的3D虚拟服装试穿系统——GS-VTON,它旨在克服现有技术局限,并提供…

ECharts饼图-富文本标签,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个饼图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供详…

【Mac 上将 MOV 格式转换为 MP4 格式的简易指南】

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

无人机之室内定位技术篇

无人机的室内定位技术是实现无人机在室内环境中精准导航和定位的关键技术。由于室内环境复杂&#xff0c;卫星导航系统&#xff08;如GPS&#xff09;无法提供有效的信号&#xff0c;因此需要依赖其他室内定位技术。 一、主要技术类型 基于视觉的定位技术 原理&#xff1a;利…

智联招聘×Milvus:向量召回技术提升招聘匹配效率

01. 业务背景 在智联招聘平台&#xff0c;求职者和招聘者之间的高效匹配至关重要。招聘者可以发布职位寻找合适的人才&#xff0c;求职者则通过上传简历寻找合适的工作。在这种复杂的场景中&#xff0c;我们的核心目标是为双方提供精准的匹配结果。在搜索推荐场景下&#xff0c…

客户端与服务端通信的端口以及新增ARP缓存

客户端&#xff08;例如浏览器&#xff09;在与服务器通信时确实会使用一个随机的、高于1024的端口。不过&#xff0c;在端口转发的场景中&#xff0c;我们主要关注的是两个不同的层面&#xff1a;服务器的监听端口&#xff08;即最终目的地的端口&#xff09;和客户端的源端口…

Linux -- 进程间通信、初识匿名管道

目录 进程间通信 什么是进程间通信 进程间通信的一般规律 前言&#xff1a; 管道 代码预准备&#xff1a; 如何创建管道 -- pipe 函数 参数&#xff1a; 返回值&#xff1a; wait 函数 参数&#xff1a; 验证管道的运行&#xff1a; 源文件 test.c &#xff1a; m…

【开发语言】c++的发展前景

C作为一种历史悠久且功能强大的编程语言&#xff0c;在软件开发领域一直保持着其独特的地位和广泛的应用前景。尽管近年来出现了许多新的编程语言和技术趋势&#xff0c;但C由于其高性能、低层访问能力以及广泛的生态系统&#xff0c;在多个领域依然具有不可替代的优势。以下是…