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.