当前位置:网站首页>Compose canvas pie chart effect drawing
Compose canvas pie chart effect drawing
2022-07-23 16:59:00 【Nine stream generation】
Compose Canvas Pie chart effect drawing
This use case is based on Compose Development adopt Canvas To draw a pie chart The pie chart data display is realized, and different data can be selected by clicking .
Final effect and code
- Stereo rendering effect
- Each section can be clicked and responded
- The whole pie chart can be touched, dragged and rotated

The final code
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
}
}// Angle calculation
}
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 // Customize View Width Here is a square So the width and height are consistent
val drawHeight = 60f // Internal line height That is, the width of the colored part
val selecyAddHeight = 10f // Increase the height after clicking
val partpadding = 3f // Spacing between parts
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 // Middle text font size
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()))
// coordinate 1,2 Quadrant return -180~0 3,4 Quadrant return 0~180
if (x < 0 && y < 0 || x > 0 && y < 0) {
//1,2 quadrant
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 ->
// Drag to start
dragstartx = offset.x
dragstarty = offset.y
},
onDragEnd = {
// Drag end
},
onDragCancel = {
// Drag cancel
},
onDrag = {
change: PointerInputChange, dragAmount: Offset ->
// Dragging
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
}// Split the coordinate components to judge the pros and cons
dragOffest = dragOffest + x + y
}
)
}
) {
translate(0f, 0f) {
start = true// Start animating
var startAngle = dragOffest// Initial angle
var selectAngle = 0f// Record the initial angle of the clicked item
for ((i, p) in point.withIndex()) {
val sweepAngle = p / sum * 360f// Biased angle
if (angles.size < point.size) {
angles.add(sweepAngle)
}
if (position != i) {
drawArc(
color = color[i],
startAngle = startAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
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, // A sign indicating whether the arc closes the center of the boundary
// style
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
}
// Middle text drawing
var textweight = paint.measureText(" All ")
var text = "$sum"
var pointweight = paint.measureText(text)
if (position != -1) {
// Selected content
val sweepAngle = point[position] / sum * 360f
drawArc(
color = color[position],
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
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
)
)// Select the back width
drawArc(
color = color[position],
alpha = 0.5f,
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
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)
)
// Middle text drawing
textweight = paint.measureText(labels[position])
val pointF = "%.1f".format(sweepAngle * 100 / 360)
val positionF = point[position]
text = "$positionF($pointF%)"
pointweight = paint.measureText(text)
// Use native Canvas To draw
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
)
}
// Use native Canvas To draw
drawContext.canvas.nativeCanvas.drawText(
if (position == -1) " All " else labels[position],
ViewWeight / 2 - textweight / 2,
ViewWeight / 2,
paint
)
drawContext.canvas.nativeCanvas.drawText(
text,
ViewWeight / 2 - pointweight / 2,
ViewWeight / 2 + paint.textSize,
paint
)
}
}
}
The actual use
@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(" having dinner ", " sleep ", " Playing CARDS ", " Go to the supermarket ", " Go to work ", " Wages ")
val color = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Gray, Color.Red, Color.Cyan)
PieChart(" Proportion of total expenditure ",color, point, labels)
}
}
Code analysis
If you want to know more about how I achieved , You can continue to read and understand
Circle drawing
Because the pie chart is round , So we put all the data together as a circle , The proportion of each piece of data inside , Is its radian , For radians, we can use Canvas Medium drawArc You can draw an arc
- color Draw colors
- startAngle Starting angle
- sweepAngle Arc passing angle
- useCenter Whether the arc closes the mark of the boundary Center
- style style Here we use the thickness of the set style to set the width of the outer circle , That is, the width of the colored part in the figure below (drawHeight)
- size The full circle size of the overall radian It mainly controls the distance between the arc and the center point
- topLeft The position of the base point of the upper left corner Because considering drawing in a circle So the distance correction is carried out
drawArc(
color = color[i],
startAngle = startAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,//1f Make up for the lack of accuracy
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
style = Stroke(width = drawHeight, miter = 10f, cap = StrokeCap.Butt),
size = Size(ViewWeight - drawHeight * 2, ViewWeight - drawHeight * 2),
topLeft = Offset(drawHeight, drawHeight)
)
Then calculate the offset angle of each part of the data in turn , And then add it all up , Then the whole circle can be spliced and drawn .
Stereoscopic effect
Again , We draw a small arc on the inner circle , And set the transparency , Finally, separate each part to have a three-dimensional effect . The following is the drawing of the inner arc , The angle is basically the same , As follows partpadding Is the separation angle
drawArc(
color = color[i],
startAngle = startAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
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, // A sign indicating whether the arc closes the center of the boundary
// style
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)
)

Drag rotation
Use touch gestures , Take the center point of the original drawing as the benchmark , Split each moving component , To judge whether the interval is for x Weight and y The weight should be increased by adding or subtracting , for instance , When at the bottom of the pie chart ( Below half the height ), The direction of movement is timing , Its growth rate is positive , This direction is clockwise , So the angle can be increased , When on the left side of the pie chart ( Half the width is left ), When the moving direction is down , Its growth rate is negative , This direction is counterclockwise , So the angle needs to be reduced .
var dragstartx = 0f
var dragstarty = 0f
detectDragGestures(
onDragStart = {
offset ->
// Drag to start
Log.d(
"pointerInput",
"onDragStart: ${
offset.x} : ${
offset.y}"
)
dragstartx = offset.x
dragstarty = offset.y
},
onDragEnd = {
// Drag end
},
onDragCancel = {
// Drag cancel
},
onDrag = {
change: PointerInputChange, dragAmount: Offset ->
// Dragging
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
}// Split the coordinate components to judge the pros and cons
dragOffest = dragOffest + x + y
}
)
}
Click to enlarge
As shown in the figure above, we can see that some areas will be enlarged after clicking , The logic is also very simple , Where you need to zoom in We draw at the end , On the one hand, it can ensure that this part will be at the upper level compared with other parts , On the other hand, we can use different widths when drawing radians , Then adjust the position , The enlarged part is finished .
drawArc(
color = color[position],
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
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
)
)// Select the back width
drawArc(
color = color[position],
alpha = 0.5f,
startAngle = selectAngle + partpadding,
sweepAngle = sweepAngle * sweepPre - partpadding,
useCenter = false, // A sign indicating whether the arc closes the center of the boundary
// style
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)
)

Click to listen , We can use gestures to monitor
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()))
// coordinate 1,2 Quadrant return -180~0 3,4 Quadrant return 0~180
if (x < 0 && y < 0 || x > 0 && y < 0) {
//1,2 quadrant
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()
}
)
Angle position judgment , If the offset angle just falls within the position range of this part , Indicates that it hits and returns
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
}
}// Angle calculation
}
return -1
}
Arrange elements
Last , adopt compose The combination of , Encapsulate and display the title and arrangement elements
@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)
)
}
}// Loop to add the right arrangement elements
}
}// Arrange on the right
}
}

Last
I think it's good , Collection , Like or follow a wave ! Everyone grows and gains together !

边栏推荐
猜你喜欢

48:第五章:开发admin管理服务:1:创建子工程【imooc-news-dev-service-admin】,管理服务模块;

【TensorFlow】检测TensorFlow GPU是否可用

Detector: detect objects with recursive feature pyramid and switchable atolos convolution

ROS2自学笔记:Rviz可视化工具

Chen Wei, head of CPU technology ecology of Alibaba pingtouge: the development road of pingtouge

零基础怎么自学软件测试?十年测试老鸟最强软件测试学习路线图

Lake Shore—EMPX-H2 型低温探针台

UPC 2022暑期个人训练赛第12场(B 组合数)

Case analysis of building campus information management system with low code

解决data functions should return an object 并(Property “visible“ must be accessed with “$data.visible“)
随机推荐
How to buy financial products with a return of more than 6%?
Numpy 数据分析基础知识第一阶段(NumPy基础)
熵权法优化TOPSIS(MATLAB)
Is it safe for online account managers to open accounts when choosing securities companies in flush
Object.defineProperty方法、数据代理
软件测试计划包括哪些内容,测试计划如何编写。分享测试计划模板
Direct exchange
benthos杂记
分类模型——逻辑回归、Fisher线性判别(SPSS)
启牛商学院上面开户安全不
【Web漏洞探索】SQL注入漏洞
Scale Match for Tiny Person Detection
Tan Zhangxi, director of risc-v Foundation: risc-v has gradually expanded from the edge to the center
Heartless sword English Chinese bilingual poem 006. to my wife
Priyanka Sharma, general manager of CNCF Foundation: read CNCF operation mechanism
零基础怎么自学软件测试?十年测试老鸟最强软件测试学习路线图
Tips and tricks for Neural Networks 深度学习训练神经网络的技巧总结(不定期更新)
Squeeze and incentive networks
僧多粥少?程序员要怎样接私活才能有效提高收入?
ROS2自学笔记:Rviz可视化工具