当前位置:网站首页>Compose Canvas饼图效果绘制
Compose Canvas饼图效果绘制
2022-07-23 13:27:00 【玖流之辈】
Compose Canvas饼图效果绘制
本用例基于Compose开发 通过Canvas来绘制饼图 实现了饼图数据展示并可进行点击选择不同的数据。
最终效果和代码
- 立体绘制效果
- 各板块可点击和响应
- 整体饼图可进行触摸拖拽和旋转

最终代码
import android.content.Context
import android.graphics.Paint
import android.widget.Toast
import androidx.compose.animation.core.FloatTweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.ftd.journal.utils.px2dip
private fun getPositionFromAngle(angles: List<Float>, touchAngle: Double, startAngle: Float): Int {
var totalanagle = 0f
for ((i, angle) in angles.withIndex()) {
totalanagle += angle
if (touchAngle >= startAngle && touchAngle <= (startAngle + totalanagle) % 360) {
return i
} else if (startAngle + totalanagle > 360) {
if (touchAngle >= startAngle || touchAngle < (startAngle + totalanagle) % 360) {
return i
}
}//角度计算
}
return -1
}
@Composable
fun PieChart(title:String,color: List<Color>, point: List<Float>, labels: List<String>) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(title)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
CenterPieChart(context = LocalContext.current, color, point, labels)
Column(
modifier = Modifier
.padding(start = 30.dp, end = 10.dp), horizontalAlignment = Alignment.Start
) {
for ((i, p) in point.withIndex()) {
Row(horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(10.dp, 10.dp)
.background(color = color[i])
)
Text(
text = "${
labels[i]} ($p)",
fontSize = 12.sp,
modifier = Modifier.padding(start = 5.dp, end = 5.dp)
)
}
}
}
}
}
}
@Composable
fun CenterPieChart(context: Context, color: List<Color>, point: List<Float>, labels: List<String>) {
val sum = point.sum()
val ViewWeight = 400f //自定义View宽度 此处为正方形 所以宽高一致
val drawHeight = 60f //内部线高 即有颜色部分的宽度
val selecyAddHeight = 10f //点击后增加的高度
val partpadding = 3f //各部分之间的间距
val angles = mutableListOf<Float>()
var start by remember {
mutableStateOf(false) }
var position by remember {
mutableStateOf(0) }
var dragOffest by remember {
mutableStateOf(0f) }
val sweepPre by animateFloatAsState(
targetValue = if (start) 1f else 0f,
animationSpec = FloatTweenSpec(duration = 1000)
)
val paint = Paint()
paint.color = Color.Black.toArgb()
paint.textSize = 28f //中间文本字体大小
paint.style = Paint.Style.STROKE
Canvas(
modifier = Modifier
.width(px2dip(LocalContext.current, ViewWeight))
.height(px2dip(LocalContext.current, ViewWeight))
.background(Color.White)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
val x = it.x - ViewWeight / 2
val y = it.y - ViewWeight / 2
var touchAngle = Math.toDegrees(Math.atan2(y.toDouble(), x.toDouble()))
//坐标1,2象限返回-180~0 3,4象限返回0~180
if (x < 0 && y < 0 || x > 0 && y < 0) {
//1,2象限
touchAngle += 360
}
val nowpostion =
getPositionFromAngle(
touchAngle = touchAngle,
angles = angles,
startAngle = dragOffest % 360
)
if (nowpostion == position) {
position = -1;
} else {
position = nowpostion;
}
Toast
.makeText(
context,
"onTap: $position",
Toast.LENGTH_SHORT
)
.show()
}
)
}
.pointerInput(Unit) {
var dragstartx = 0f
var dragstarty = 0f
detectDragGestures(
onDragStart = {
offset ->
// 拖动开始
dragstartx = offset.x
dragstarty = offset.y
},
onDragEnd = {
// 拖动结束
},
onDragCancel = {
// 拖动取消
},
onDrag = {
change: PointerInputChange, dragAmount: Offset ->
// 拖动中
dragstartx += dragAmount.x
dragstarty += dragAmount.y
var x = 0f
var y = 0f
if (dragstarty < ViewWeight / 2) {
x = dragAmount.x
} else {
x = -dragAmount.x
}
if (dragstartx < ViewWeight / 2) {
y = -dragAmount.y
} else {
y = dragAmount.y
}//拆分坐标分量来进行顺逆的判断
dragOffest = dragOffest + x + y
}
)
}
) {
translate(0f, 0f) {
start = true//开始绘制动画
var startAngle = dragOffest//初始角度
var selectAngle = 0f//记录被点击项的初始角度
for ((i, p) in point.withIndex()) {
val sweepAngle = p / sum * 360f//偏向的角度
if (angles.size < point.size) {
angles.add(sweepAngle)
}
if (position != i) {
drawArc(
color = color[i],
startAngle = startAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 2, ViewWeight - drawHeight * 2),
topLeft = Offset(drawHeight, drawHeight)
)
drawArc(
color = color[i],
alpha = 0.5f,
startAngle = startAngle + partpadding / 2,
sweepAngle = sweepAngle * sweepPre - partpadding / 2,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight / 5, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 3, ViewWeight - drawHeight * 3),
topLeft = Offset(drawHeight * 1.5f, drawHeight * 1.5f)
)
} else {
selectAngle = startAngle;
}
startAngle += sweepAngle
}
//中间文本绘制
var textweight = paint.measureText("全部")
var text = "$sum"
var pointweight = paint.measureText(text)
if (position != -1) {
//选择的内容
val sweepAngle = point[position] / sum * 360f
drawArc(
color = color[position],
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(
width = drawHeight + selecyAddHeight,
miter = 10f,
cap = StrokeCap.Butt
),
size = Size(
ViewWeight - drawHeight * 2 + selecyAddHeight,
ViewWeight - drawHeight * 2 + selecyAddHeight
),
topLeft = Offset(
drawHeight - selecyAddHeight / 2,
drawHeight - selecyAddHeight / 2
)
)//选择后宽度
drawArc(
color = color[position],
alpha = 0.5f,
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight / 5, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 3, ViewWeight - drawHeight * 3),
topLeft = Offset(drawHeight * 1.5f, drawHeight * 1.5f)
)
//中间文本绘制
textweight = paint.measureText(labels[position])
val pointF = "%.1f".format(sweepAngle * 100 / 360)
val positionF = point[position]
text = "$positionF($pointF%)"
pointweight = paint.measureText(text)
//用原生Canvas来绘制
drawContext.canvas.nativeCanvas.drawText(
labels[position],
ViewWeight / 2 - textweight / 2,
ViewWeight / 2,
paint
)
drawContext.canvas.nativeCanvas.drawText(
text,
ViewWeight / 2 - pointweight / 2,
ViewWeight / 2 + paint.textSize,
paint
)
}
//用原生Canvas来绘制
drawContext.canvas.nativeCanvas.drawText(
if (position == -1) "全部" else labels[position],
ViewWeight / 2 - textweight / 2,
ViewWeight / 2,
paint
)
drawContext.canvas.nativeCanvas.drawText(
text,
ViewWeight / 2 - pointweight / 2,
ViewWeight / 2 + paint.textSize,
paint
)
}
}
}
实际使用
@Composable
fun Content() {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
val point = listOf(10f, 40f, 20f, 80f, 100f, 60f)
val labels = listOf("吃饭", "睡觉", "打牌", "去超市", "上班", "工资")
val color = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Gray, Color.Red, Color.Cyan)
PieChart("总支出占比",color, point, labels)
}
}
代码剖析
如果还想进一步了解我是怎么实现的,可往下继续阅读和了解
圆形绘制
因为饼图整体呈圆形,所以我们将所有数据合起来当一个圆,里面的每一块数据的占比,就是它的弧度,而对于弧度我们可以用Canvas中的drawArc 可以来绘制器弧形
- color 绘制颜色
- startAngle 开始角度
- sweepAngle 弧形经过角度
- useCenter 圆弧是否闭合边界中心的标志
- style 样式 这里我们利用设定样式的粗细来设置外距圆的宽度,即下图有颜色的部分的宽(drawHeight)
- size 整体弧度的完整圆大小 主要控制弧度距离中心点的距离
- topLeft 左上角基点的位置 因为考虑到在圆内绘制 所以进行了距离矫正
drawArc(
color = color[i],
startAngle = startAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,//1f 弥补部分精度不足问题
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 2, ViewWeight - drawHeight * 2),
topLeft = Offset(drawHeight, drawHeight)
)
然后依次计算数据中每个部分的偏移角度,然后全部相加,那么最后整个圆就能拼接绘制出来。
立体效果
同样,我们在最内圈画一成小的弧形,并设置透明度,最后再分隔开每一个部分就能有立体的效果了。如下为内弧的画发,其角度基本是一致的,如下的partpadding就是分隔角度
drawArc(
color = color[i],
startAngle = startAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 2, ViewWeight - drawHeight * 2),
topLeft = Offset(drawHeight, drawHeight)
)
drawArc(
color = color[i],
alpha = 0.5f,
startAngle = startAngle + partpadding / 2,
sweepAngle = sweepAngle * sweepPre - partpadding / 2,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight / 5, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 3, ViewWeight - drawHeight * 3),
topLeft = Offset(drawHeight * 1.5f, drawHeight * 1.5f)
)

拖移旋转
利用触摸手势,以原图中心点作为基准,将每次移动的分量进行拆分,来判断该区间对于x分量和y分量应该采取加或减的增幅方式,举个例子,当处于饼图下方时(一半高度以下),移动方向为正时,其增幅为正,该方向为顺时针方向,所以角度可以增加,当处于饼图左侧时(一半宽度偏左),移动方向为下时,其增幅为负,该方向为逆时针方向,所以角度需要减少。
var dragstartx = 0f
var dragstarty = 0f
detectDragGestures(
onDragStart = {
offset ->
// 拖动开始
Log.d(
"pointerInput",
"onDragStart: ${
offset.x} : ${
offset.y}"
)
dragstartx = offset.x
dragstarty = offset.y
},
onDragEnd = {
// 拖动结束
},
onDragCancel = {
// 拖动取消
},
onDrag = {
change: PointerInputChange, dragAmount: Offset ->
// 拖动中
Log.d(
"pointerInput",
"onDrag: $dragAmount"
)
dragstartx += dragAmount.x
dragstarty += dragAmount.y
var x = 0f
var y = 0f
if (dragstarty < ViewWeight / 2 ) {
x=dragAmount.x
}else{
x=-dragAmount.x
}
if (dragstartx < ViewWeight / 2 ) {
y=-dragAmount.y
}else{
y=dragAmount.y
}//拆分坐标分量来进行顺逆的判断
dragOffest = dragOffest + x + y
}
)
}
点击放大
如上图我们能看到部分区域点击后会进行放大,其逻辑其实也非常简单,在需要放大的位置 我们在最后进行绘制,一方面是可以保证该部分会处于相较于其他部分的上层,另一方面我们对其在绘制弧度时的可以使用不同的宽度,然后再调整一下位置,放大的部分就完成了。
drawArc(
color = color[position],
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(
width = drawHeight + selecyAddHeight,
miter = 10f,
cap = StrokeCap.Butt
),
size = Size(
ViewWeight - drawHeight * 2 + selecyAddHeight,
ViewWeight - drawHeight * 2 + selecyAddHeight
),
topLeft = Offset(
drawHeight - selecyAddHeight / 2,
drawHeight - selecyAddHeight / 2
)
)//选择后宽度
drawArc(
color = color[position],
alpha = 0.5f,
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // 指示圆弧是否闭合边界中心的标志
// 样式
style = Stroke(width = drawHeight / 5, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 3, ViewWeight - drawHeight * 3),
topLeft = Offset(drawHeight * 1.5f, drawHeight * 1.5f)
)

其中点击的监听,我们采用手势进行监听即可
detectTapGestures(
onTap = {
Log.d(
"pointerInput",
"onTap: ${
it.x - ViewWeight / 2} ${
it.y - ViewWeight / 2}"
)
val x = it.x - ViewWeight / 2
val y = it.y - ViewWeight / 2
var touchAngle = Math.toDegrees(Math.atan2(y.toDouble(), x.toDouble()))
//坐标1,2象限返回-180~0 3,4象限返回0~180
if (x < 0 && y < 0 || x > 0 && y < 0) {
//1,2象限
touchAngle += 360
}
val nowpostion =
getPositionFromAngle(touchAngle = touchAngle, angles = angles, startAngle = dragOffest%360)
if (nowpostion == position) {
position = -1;
} else {
position = nowpostion;
}
Toast
.makeText(
context,
"onTap: $position",
Toast.LENGTH_SHORT
)
.show()
}
)
角度位置判断,如果偏移角度刚好落于该部分位置区间内,表示其命中并返回
private fun getPositionFromAngle(angles: List<Float>, touchAngle: Double,startAngle:Float): Int {
var totalanagle=0f
for ((i, angle) in angles.withIndex()) {
totalanagle+=angle
if (touchAngle>=startAngle&&touchAngle<=(startAngle+totalanagle)%360){
return i
}else if (startAngle+totalanagle>360){
if (touchAngle>=startAngle||touchAngle<(startAngle+totalanagle)%360){
return i
}
}//角度计算
}
return -1
}
排列元素
最后,通过compose的组合,将标题和排列元素进行封装处理和展示
@Composable
fun PieChart(title:String,color: List<Color>, point: List<Float>, labels: List<String>) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(title)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
CenterPieChart(context = LocalContext.current, color, point, labels)
Column(
modifier = Modifier
.padding(start = 30.dp, end = 10.dp), horizontalAlignment = Alignment.Start
) {
for ((i, p) in point.withIndex()) {
Row(horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(10.dp, 10.dp)
.background(color = color[i])
)
Text(
text = "${
labels[i]} ($p)",
fontSize = 12.sp,
modifier = Modifier.padding(start = 5.dp, end = 5.dp)
)
}
}//循环添加右边排列元素
}
}//右边排列
}
}

最后
觉得不错的,收藏,点赞或关注一波呗!大家一起成长收获!

边栏推荐
- 僧多粥少?程序员要怎样接私活才能有效提高收入?
- Heartless sword English Chinese bilingual poem 006. to my wife
- Summary of after class homework of Microcomputer Principle and technical interface
- O3DF执行董事Royal O’Brien:开源没有边界,所有共享的声音都会变成实际方向
- Object.defineProperty方法、数据代理
- Notes on Microcomputer Principle and technical interface
- Tan Zhangxi, director of risc-v Foundation: risc-v has gradually expanded from the edge to the center
- js如果小数是0就减去,不是就保留
- 三方支付公司有哪些?
- Bag of Tricks for Image Classification with Convolutional Neural Networks(卷积神经网络在图像分类中的技巧)
猜你喜欢
随机推荐
Docker install redis
Using "soup.h1.text" crawler to extract the title will be one more\
UiPath Studio Enterprise 22.4 Crack
启牛商学院上面开户安全不
The working principle of PLL. For example, how can our 8MHz crystal oscillator make MCU work at 48mhz or 72mhz
快速学会使用文件的权限
MATLAB基础
48:第五章:开发admin管理服务:1:创建子工程【imooc-news-dev-service-admin】,管理服务模块;
【Error】TypeError: expected str, bytes or os.PathLike object, not int
SSD: Single Shot MultiBox Detector
【C语言】结构体、枚举和联合体
SSD: Single Shot MultiBox Detector
解决data functions should return an object 并(Property “visible“ must be accessed with “$data.visible“)
怎么购买收益在6%以上的理财产品?
Introduction to Huawei's new version of datacom certification
FreeRTOS个人笔记-挂起/解挂任务
目前有哪些年利率6%左右的保本理财产品?
层次分析法(MATLAB)
COPU副主席刘澎:中国开源在局部领域已接近或达到世界先进水平
Notes on Microcomputer Principle and technical interface









