This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!
When developing a project, it is inevitable to encounter the situation that the native controls cannot be satisfied and need to be customized. Today, I will draw a few charts to practice the custom View in Jetpack Compose.
Line diagram
The drawing principle is the same as in the previous XML, but the implementation has changed a little, much simpler than before, such as the following path to draw a line graph. Once you’ve built your path, just draw it on the Canvas.
If you want to zoom the icon with two fingers, you can listen for gestures using Modifier.Graphicslayer ().transformable(). To monitor the size of the finger zoom by rememberTransformableState then returns the value can be assigned to the corresponding variables
Complete code:
data class Point(val X: Float = 0f.val Y: Float = 0f)
@Composable
fun LineChart(a) {
// To record the scale size
var scale by remember { mutableStateOf(1f)}val state = rememberTransformableState {
zoomChange, panChange, rotationChange ->
scale*=zoomChange
}
val point = listOf(
Point(10f.10f), Point(50f.100f), Point(100f.30f),
Point(150f.200f), Point(200f.120f), Point(250f.10f),
Point(300f.280f), Point(350f.100f), Point(400f.10f),
Point(450f.100f), Point(500f.200f))val path = Path()
for ((index, item) in point.withIndex()) {
if (index == 0) {
path.moveTo(item.X*scale, item.Y)
} else {
path.lineTo(item.X*scale, item.Y)
}
}
val point1 = listOf(
Point(10f.210f), Point(50f.150f), Point(100f.130f),
Point(150f.200f), Point(200f.80f), Point(250f.240f),
Point(300f.20f), Point(350f.150f), Point(400f.50f),
Point(450f.240f), Point(500f.140f))val path1 = Path()
path1.moveTo(point1[0].X*scale, point1[0].Y)
path1.cubicTo(point1[0].X*scale, point1[0].Y, point1[1].X*scale, point1[1].Y, point1[2].X*scale, point1[2].Y)
path1.cubicTo(point1[3].X*scale, point1[3].Y, point1[4].X*scale, point1[4].Y, point1[5].X*scale, point1[5].Y)
path1.cubicTo(point1[6].X*scale, point1[6].Y, point1[7].X*scale, point1[7].Y, point1[8].X*scale, point1[8].Y)
path1.cubicTo(point1[7].X*scale, point1[7].Y, point1[8].X*scale, point1[8].Y, point1[9].X*scale, point1[9].Y)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.background(Color.White)
// Listen for gesture zooming
.graphicsLayer(
).transformable(state)
) {
// Draw the X-axis and Y-axis
drawLine(
start = Offset(10f.300f),
end = Offset(10f.0f),
color = Color.Black,
strokeWidth = 2f
)
drawLine(
start = Offset(10f.300f),
end = Offset(510f.300f),
color = Color.Black,
strokeWidth = 2f
)
/ / draw the path
drawPath(
path = path,
color = Color.Blue,
style = Stroke(width = 2f)
)
drawPath(
path = path1,
color = Color.Green,
style = Stroke(width = 2f))}}Copy the code
A histogram
Now let’s draw the bar chart. Drawing is easy, just draw the rectangle according to the coordinates. The API for drawing rectangles in Jetpack Compose is different from the previous API in XML. You need to provide the upper left corner of the drawing and the size of the rectangle to draw, just take a look at the constructor.
Then add the column click event, Jetpack Compose listening click on the screen position coordinates using the pointerInput method in the Modifier, and then determine whether the click coordinates in the rectangle range, the following code only to determine the X axis coordinates, can also add the Y axis of the judgment.
Finally, animate the bar chart using the animateFloatAsState method. Set the values 0 to 1 to represent the percentage of the height currently drawn, and then add the percentage value to the height as you draw.
Complete code:
private fun identifyClickItem(points: List<Point>, x: Float, y: Float): Int {
for ((index, point) in points.withIndex()) {
if (x > point.X+20 && x < point.X + 20+40) {
return index
}
}
return -1
}
@Composable
fun BarChart(a) {
val point = listOf(
Point(10f.10f), Point(90f.100f), Point(170f.30f),
Point(250f.200f), Point(330f.120f), Point(410f.10f),
Point(490f.280f), Point(570f.100f), Point(650f.10f),
Point(730f.100f), Point(810f.200f))var start by remember { mutableStateOf(false)}val heightPre by animateFloatAsState(
targetValue = if (start) 1f else 0f,
animationSpec = FloatTweenSpec(duration = 1000)
)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.background(Color.White)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
val i = identifyClickItem(point, it.x, it.y)
Log.d("pointerInput"."onTap: ${it.x} ${it.y} item:$i")
Toast
.makeText(this@FourActivity."onTap: $i", Toast.LENGTH_SHORT)
.show()
}
)
}
) {
// Draw the X-axis and Y-axis
drawLine(
start = Offset(10f.600f),
end = Offset(10f.0f),
color = Color.Black,
strokeWidth = 2f
)
drawLine(
start = Offset(10f.600f),
end = Offset(850f.600f),
color = Color.Black,
strokeWidth = 2f
)
start = true
for (p in point) {
drawRect(
color = Color.Blue,
topLeft = Offset(p.X + 20.600 - (600 - p.Y) * heightPre),
size = Size(40f, (600 - p.Y) * heightPre)
)
}
}
}
Copy the code
The pie chart
Finally, draw a pie chart. The pie chart can be realized by drawing drawPath and drawArc. DrawArc is simpler.
Add a click event to each piece of pie chart. The click event is also the coordinate that listens for the click in the Modifier’s pointerInput method. Math.atan2() returns the radian value of the line from the origin (0,0) to (x,y) and the positive X-axis, then converts the radian to an Angle using the math.todegrees () method, and finally gets the region of the click from the Angle.
Complete code:
private fun getPositionFromAngle(angles:List<Float>,touchAngle:Double):Int{
var totalAngle = 0f
for ((i, angle) in angles.withIndex()) {
totalAngle +=angle
if(touchAngle<=totalAngle){
return i
}
}
return -1
}
@Composable
fun PieChart(a) {
val point = listOf(10f.40f.20f.80f.100f.60f)
val color = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Gray, Color.Red, Color.Cyan)
val sum = point.sum()
var startAngle = 0f
val radius = 200f
val rect = Rect(Offset(-radius, -radius), Size(2 * radius, 2 * radius))
val path = Path()
val angles = mutableListOf<Float> ()val regions = mutableListOf<Region>()
var start by remember { mutableStateOf(false)}val sweepPre by animateFloatAsState(
targetValue = if (start) 1f else 0f,
animationSpec = FloatTweenSpec(duration = 1000)
)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.White)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
Log.d(
"pointerInput"."onTap: ${it.x - radius.toInt()} ${it.y - radius.toInt()} ${regions}"
)
var x = it.x - radius
var y = it.y - radius
var touchAngle = Math.toDegrees(Math.atan2(y.toDouble(),x.toDouble()))
// quadrant 1,2 returns -180~0. Quadrant 4 returns 0~180
if(x<0&&y<0 || x>0&&y<0) {/ / 1, 2 quadrant
touchAngle += 360;
}
val position = getPositionFromAngle(touchAngle = touchAngle,angles = angles)
Toast
.makeText(
this@FourActivity."onTap: $position",
Toast.LENGTH_SHORT
)
.show()
}
)
}
) {
translate(radius, radius) {
start = true
for ((i, p) in point.withIndex()) {
var sweepAngle = p / sum * 360f
println("sweepAngle: $sweepAngle p:$p sum:$sum")
path.moveTo(0f.0f)
path.arcTo(rect = rect, startAngle, sweepAngle*sweepPre, false)
angles.add(sweepAngle)
drawPath(path = path, color = color[i])
path.reset()
// drawArc(color = color[i],
// startAngle = startAngle,
// sweepAngle = sweepAngle,
// useCenter = true,
// topLeft = Offset(-radius,-radius),
// size = Size(2*radius,2*radius)
/ /)
startAngle += sweepAngle
}
}
}
}
Copy the code
Jetpack Compose has just come out and some functions are not perfect yet. You can use the original canvas in the scope of drawIntoCanvas and draw in the original way. The object in the drawIntoCanvas scope is a canvas. The it. NativeCanvas method returns a native Android Canvas object. And then we can use it to draw exactly the way we did before.
For example, in the above pie chart click event, we can calculate the drawing Region of each piece by combining the two classes of Path and Region. However, it is found that there is no corresponding Region class in the UI package of Jetpack Compose, and only the corresponding Path class. If you want to use the above functions, you can only use the original Path class and Region to calculate. The usage is as follows:
@Composable
fun PieChart1(a){
val point = listOf(10f.40f.20f.80f.100f.60f)
val colors = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Gray, Color.Red, Color.Cyan)
val sum = point.sum()
var startAngle = 0f
val radius = 200f
val path = android.graphics.Path()
val rect = android.graphics.RectF(-radius,-radius,radius,radius)
val regions = mutableListOf<Region>()
val paint = Paint()
paint.isAntiAlias = true
paint.style = Paint.Style.FILL
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.White)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
Log.d(
"pointerInput"."onTap: ${it.x - radius.toInt()} ${it.y - radius.toInt()} ${regions.toString()}"
)
val x = it.x - radius
val y = it.y - radius
var position = -1
for ((i, region) in regions.withIndex()) {
if(region.contains(x.toInt(),y.toInt())){
position = i
}
}
Toast
.makeText(
this@FourActivity."onTap: $position",
Toast.LENGTH_SHORT
)
.show()
}
)
}
) {
translate(radius, radius) {
drawIntoCanvas {
for ((i, p) in point.withIndex()) {
var sweepAngle = p / sum * 360f
println("sweepAngle: $sweepAngle p:$p sum:$sum")
path.moveTo(0f.0f)
path.arcTo(rect,startAngle,sweepAngle)
// Calculate the draw area and save it
val r = RectF()
path.computeBounds(r,true)
val region = Region()
region.setPath(path, Region(r.left.toInt(),r.top.toInt(),r.right.toInt(),r.bottom.toInt()))
regions.add(region)
paint.color = colors[i].toArgb()
it.nativeCanvas.drawPath(path,paint)
path.reset()
startAngle += sweepAngle
}
}
}
}
}
Copy the code
The result is the same as the pie chart drawn earlier.
Summary: The custom View API in Jetpack Compose is much more concise than the original one. In addition, when the current API cannot meet the requirements, the original API can also be used for drawing conveniently, and the experience is very good.