preface

The code is not difficult, so I’m going to use Kotlin to implement it for increased proficiency

Let’s see what we’re doing. Okay

So with that, let’s get our heads together

  • The pie chart is centered, and each area is a sector, which needs to be drawn by Canvas. drawArc according to the Angle
  • Path. arcTo needs to be positioned halfway to the fan radian to draw the starting point of the polyline
  • Draw a line using canvas.drawPath. The length of the line is proportional to the pie chart size
  • DrawText using canvas.drawText. The size of the text is proportional to the pie chart size, and the width of the text needs to be calculated when the text is drawn

After thinking clearly, roll up your sleeves and refuel

knowledge

Let’s start with an idea. When we paint a sector, where should the degree be?

It should make sense when you see the picture

Draw the pie chart

Let’s start by looking at the parameters. Obviously, the left, top, right, and bottom parameters form a palette. StartAngle is the starting Angle,sweepAngle is how many degrees to draw from the starting Angle, useCenter is whether to attach to the center of the circle, and paint is the brush

 public void drawArc(float left, float top, float right, float bottom, float startAngle,
            float sweepAngle, boolean useCenter, @NonNull Paint paint) {
        super.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);
    }
Copy the code

We use the width and height of the current control as panels to draw a circular pie chart

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawArc(0f, 0f, width, height, 0f, 360f, true, paintRed)
    }
Copy the code

If we want to draw a round pie chart, we have to make sure that left=top=right=bottom

Center the pie chart

/ * * * * / var width: the width of the view Float = 0 f/view * * * * / var height: the height of the Float = 0 f / * * * * / var drawArc distance to the left of the distance left: Float = 0f /** * drawArc distance to the top */ var top: Float = 0f /** * drawArc distance to the right */ var right: Float = 0f /** * drawArc distance to the top */ var right: Float = 0f /** * drawArc distance to the top */ var right: Float = 0f /** * drawArc; Float = 0f /** * drawArc; Float = 0f @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawArc(left, top, right, bottom, 0f, 360f, true, paint) } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) setBackgroundColor(resources.getColor(R.color.black)) width = w.toFloat() height = h.toFloat() left = width / 4f top = width / 4f right = width - left bottom = width - top }Copy the code

Perfect center

Next, we will draw the top from 0 degrees to 360 degrees in several more steps

    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        ...
        canvas.drawArc(left, top, right, bottom, 0f, 20f, true, paintPuple)
        canvas.drawArc(left, top, right, bottom, 20f, 10f, true, paintGray)
        canvas.drawArc(left, top, right, bottom, 30f, 40f, true, paintGreen)
        canvas.drawArc(left, top, right, bottom, 70f, 110f, true, paintBlue)
        canvas.drawArc(left, top, right, bottom, 180f, 110f, true, paintRed)
        canvas.drawArc(left, top, right, bottom, 290f, 70f, true, paintYellow)
    }
Copy the code

Not bad

The degree above is written dead, now let’s write it alive

Give a set of names. If a farmer sells fruit, pears sell 10, bananas sell 3, apples sell 7, then the set of names is pieList=(10,3,7). Since the pie chart is drawn according to angles, we have to convert this set of numbers into a set of angles. During the conversion process, we need to know the proportion of each fruit in the total fruit, and then multiply 360 degrees by this proportion to know the degree of each fruit. The ratio of pears is 10/(10+3+7)=1/2, and the degree of pears in the pie chart is 1/2*360=180 degrees. According to this calculation, the degree of bananas and apples in the pie chart are 54 degrees and 126 degrees respectively, so the distribution of the pie chart is also out

Now, let’s define a set of numbers, figure out the set of proportions and the set of degrees, and here’s the set of proportions, and the set of degrees that we’ll figure out as we draw

/** * Personal category set */ var pieList = arrayListOf(10f,3f,7f) /** * The proportion of pie charts */ var scaleList = arrayListOf<Float>() /** * Number of categories total  */ var total: Float = 0f override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, Oldh) for (a in pieList) {scalelist.add (a.div(total))}}Copy the code

We have the ratio set, then we loop the ratio value, and then multiply the ratio value by 360 degrees to calculate the Angle value for drawArc’s sweepAngle. However, we still lack a startAngle, so we can define a starting Angle of 0 degrees. Then each time according to the calculated Angle value sweepAngle to accumulate the starting degree, with the code to achieve the next

Var currentDegree: Float = 0f /** * Add degree to the pie chart as the starting degree of the next graph */ var srctorDegree: Float = 0f /** * add degree to the pie chart as the starting degree of the next graph */ Float = 0f @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun onDraw(canvas: Canvas) {super.ondraw (Canvas) for (scale in scaleList) {val paint = paint () paint.strokeWidth = dip(10.0f).tofloat () Paint. IsAntiAlias = true // Define a randomly generated number of colors, Var hex = "#" + integer.tohexString ((-16777216 * math.random ()).toint ()) paint. Color = color.parsecolor (hex) Canvas. DrawArc (left, top, right, bottom, currentDegree, srctorDegree, true) CurrentDegree += srctorDegree}}Copy the code

Ok, now we can randomly define numbers to generate pie charts of proportions

Draw the line

Next, we draw polylines, the starting point of which is half of each sector arc. The arcTo method of path can also draw circles, and the use of method parameters is the same. We can make arcTo draw with Canvas. drawArc. ArcTo’s startAngle is going to be canvas. DrawArc plus half the sweepAngle, so we’re halfway to the arc, arcTo’s sweepAngle is going to be 0, so we’re just going to position, not draw

. canvas.drawArc(left, top, right, bottom, currentDegree, srctorDegree, true, paint) val path = Path() path.arcTo(left, top, right, bottom, currentDegree + srctorDegree / 2, 0f, false) ...Copy the code

Now, path is halfway to the edge of the arc, and then we need to know the current path coordinates and draw polylines based on those coordinates,

Val bounds = RectF() // Assign the current coordinate of path to bounds path.computeBounds(bounds, true)Copy the code

Now that we have the coordinates, let’s take a look at the renderings. The broken lines and the text are in four directions. Let’s divide the pie into four regions, using the center of the circle as the origin of the coordinate axis, and divide it into four quadrants:

  • Quadrant 1: the line is on the upper right and the text is to the right of the line
  • The second quadrant: the line is upper left, and the text is to the left
  • The third quadrant: the line is lower left, and the text is to the left
  • Fourth quadrant: the line is lower right and the text is to the right of the line

So, the next step is how to determine which quadrant the current starting point is in. First, take the first quadrant as an example. If the current coordinate is more than half of the horizontal axis of the pie chart and less than half of the vertical axis of the pie chart, then it is the first quadrant

/ var slantLine: Int = 30 override fun onSizeChanged(w: Int, h: Int) Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, Oldh) // Calculate the proportion of the line lineae = (width / 30f).toint () // Calculate the proportion of the slash line slantLine = (width / 40f).toint ()} @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) for (scale in scaleList) { ... val path = Path() path.arcTo(left, top, right, bottom, currentDegree + srctorDegree / 2, 0f, false) val bounds = RectF() path.computeBounds(bounds, If (bounds. Left >= width / 2 && bounds. Top <= width / 2) {path.lineto (bounds. Left + lineae, bounds.top) path.lineTo(bounds.left + lineae + slantLine, bounds.top - slantLine) canvas.drawPath(path, Bounds. Left <= width / 2 && bounds. Top <= width / 2) {path.lineTo(bounds. Left - lineae, bounds.top) path.lineTo(bounds.left - lineae - slantLine, bounds.top - slantLine) canvas.drawPath(path, Left <= width / 2 && bounds. Top >= width / 2) {path.lineTo(bounds. Left - lineae, bounds.top) path.lineTo(bounds.left - lineae - slantLine, bounds.top + slantLine) canvas.drawPath(path, Else {path.lineto (bounds. Left + lineae, bounds. Top) path.lineto (bounds. Left + lineae + slantLine, bounds.top + slantLine) canvas.drawPath(path, paintLine) } } ... }Copy the code

Oh, here it comes

Rendering text

The next step is to draw the text, the first and fourth quadrants are fine, the text can be drawn after the polyline, but the second and third quadrants are not allowed, we have to move the text width forward in order to fit perfectly on the polyline, so let’s define a method for calculating the text

Private fun getStringWidth(STR: String): Float = paintline.measureText (STR)Copy the code

The text will change according to pie size, so set the text size ratio

    paintLine.textSize = dip(width / 100).toFloat()
Copy the code

Now start drawing the text

@RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) ... Val textStr = string. format("%.2f%%", Scale * 100) // Get the width of the text val textWidth = getStringWidth(textStr) // first quadrants if (bounds. Left >= width / 2 && bounds. Top <= width / 2) { ... canvas.drawText(textStr, bounds.left + lineae + slantLine, bounds.top - slantLine, paintText) ... } else if (bounds. Left <= width / 2 && bounds. Top <= width / 2) {... canvas.drawText(textStr, bounds.left - lineae - slantLine - textWidth, bounds.top - slantLine, paintText) ... } else if (bounds. Left <= width / 2 && bounds. Top >= width / 2) {... canvas.drawText(textStr, bounds.left - lineae - slantLine - textWidth, bounds.top + lineae, paintText) ... // quadrant 4} else {... canvas.drawText(textStr, bounds.left + lineae + slantLine, bounds.top + slantLine, paintText) ... }}Copy the code

Well, not bad.

Then we look at the effect picture, the pie chart in the middle of a black circle with the same background color, it is not simple

Color = resources.getColor(r.color.black) paintCicle. IsAntiAlias = true paintCicle. Style = Paint.Style.FILL @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) ... DrawCircle (width / 2, width / 2, width / 8, paintCicle)}Copy the code

We then expose a method to the Activity to call

Fun setPieData(a: ArrayList<Float>) {pielist.clear () pielist.addall (a) invalidate()}Copy the code

So, the Activity can be called like this

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        pie1.setPieData(arrayListOf(1f,10f,15f,9f,15f))
        pie2.setPieData(arrayListOf(3f,8f,15f,7f,9f))
        pie3.setPieData(arrayListOf(9f,3f,7f,3f,4f,2f,1f))
    }
Copy the code

conclusion

Feeling is in a word, kotlin is awesome