This article is the first nuggets (not complete orz), now rewrite and complete.

This article has been authorized hongyang public number reprint

On Android, when it comes to charts, we tend to look for libraries like MPAndroidChart.

More often than not, however, we only need a certain type of chart, and it feels a bit heavy to have to import the entire library (with all the chart logic) for that type of chart.

As the saying goes, do it yourself.

Hence the doughnut of today. (Why donuts… Don’t you think the circular pie chart looks like a donut? Haha)


The origin of

As a programmer, the origin of a new requirement/control is often derived from the product, so the birth of this control, in fact, is very simple, from a design diagram of optimization points (only part of the cut, the rest of the public should not be disclosed) :

3 paint, 3 different colors

However, DO not know why, looking at the donut is pretty beautiful, unwilling to draw out so simple…

Then a charming voice in my heart told me: “Since this is not urgent and only optimized, why not optimize it thoroughly and make it more beautiful than this product?”

Open AS, create a new Project, fill in Name, create a class inheriting View, and…

And then… He meow and then how to do !!!!

confused

I believe a lot of people write custom controls will have this doubt… I’ve created the class. I’ve inherited the View. Next, I won’t do TAT

In fact, not only you, is I have written a lot of controls, now build a class, I also occasionally have this situation… (Well, maybe not enough).

The reason is simple: all the elements are in my mind, without a specific animation effect and preview, so there are too many directions and I feel confused all of a sudden.

Because when we want to accomplish a control or animation effect, we often have a mental picture of what we want to achieve very quickly, but when we actually implement it, we don’t seem to know how to translate the effect into lines of code.

In times of confusion, WHAT I do is write down my expectations:

  • My doughnut moves
  • My donut has more configurable parameters (more degrees of freedom)
  • I want my doughnut to have an effect when I click on it, and the effect, I think, is to float up and move up on the Z-axis, preferably with a shadow
  • My donuts need to be easy to use…

When the requirements are written down, we can gradually break them down.

In view of the above demand points, we gradually determine our plan:

  • donuts
    • Inherit the View and draw it yourself
  • Donuts that move
    • When the Animation starts, it just calculates the progress step by step and lets me draw different things based on the values
  • More than a configurable
    • Because the parameters are more, so merge into a Config class, take Builder mode, form the chain configuration, maintain a clean coding style
  • Click on the effect of
    • Donuts can be sized to create a floating z-axis effect with a BlurMaskFilter or ShadowLayer
  • Simple to fit
    • Expose as few apis as possible, and program toward interfaces

data

Limited by space, only the core code is posted here, and the rest is mainly explained by ideas.

To start with, let’s try a simple donut:

3 pens, 3 angles, done.

However, if we allowed more doughnuts to be configured externally, wouldn’t that change the class? If only there was a way to dynamically add and delete.

Let’s take a look at the ingredients needed for a donut so far:

  • color
  • describe
  • Data (for proportion calculation)

In other words, if we agree with the user that the user will provide these elements and we will only render them, then we can implement dynamic additions and deletions.

So, how to make an appointment?

If we want to use a class, that is, the user must be passed on to our class, so for users, is the use of a problem, because we want to draw on the icon data server requests tend to come back, if you want to draw a donut, have to do more than one step conversion, this is undoubtedly increases the complexity, also is not what we expect is simple to use.

With this in mind, I took the interface approach. The interface constraint requires that the above three elements be returned, which has the advantage of not destroying the user’s original data. He can even use the original data bean to implement his doughnut requirements. Another advantage is that I can easily extend it.

In fact, this point has also been recognized by users (PS, the foreign library was rated as one of the 25 noteworthy libraries at the beginning of 2018, so there are a lot of foreign issues, and mainly foreign friends are contacted by email) :

According to the above conditions, we preliminarily define an interface: IPieInfo

After that, we can rely on the interface to get our calculations:

  • The color of the doughnut determines the color of the doughnut
  • Get the value to calculate the value of the doughnut
  • Gets the description to draw the description of the doughnut

This can be done without limiting the user’s data type, just by implementing our interface and returning each method.

When drawing, we store the data passed in by the user into a List. When storing, we calculate the starting Angle and ending Angle according to the value obtained, and wrap the data in a class for internal use of the control. All of this, however, is opaque to the outside world, which is only concerned with configuration, not computation.

Click on the

Donut clicking is a bit of a hassle, mainly for the following reasons:

  • Donuts support the starting Angle, but there is no requirement for the starting Angle, that is, -3600° is ok
  • When you click, you need to determine exactly which doughnut is in the area you click
  • The action after the donut is clicked, as well as the donut animation of the last click and the donut animation of this click need to switch (one restores and one floats).

Let’s start with the first question. Although our doughnut can be set to an infinite Angle, it can actually be reduced to 0 ~ 360°. Even if a large value is passed in, it is only a certain multiple * 360 + offset, so for any Angle, we need to bundle it to 0 ~ 360° :

When the click is triggered, we first judge whether the position of the click is within the doughnut (or pie chart). The judgment method is also very simple, which is to calculate the straight line distance between two points.

We obtain x and y of the touch point and calculate its distance to the center. If the current mode is doughnut (annular pie chart), the inner diameter of the doughnut ≤ distance ≤ outer diameter of the doughnut is determined to be within the doughnut.

After calculating the distance, we need to calculate the Angle, and we use the Angle to figure out which section of the doughnut we are currently clicking on.

Now that we know the xy of the click, and the center point, we can use the ATan2 method to calculate the Angle backwards

    // Get the Angle
    double touchAngle = Math.toDegrees(Math.atan2(y - centerY, x - centerX));
Copy the code

The values calculated here are in the range of -180° to 180°, that is, starting from the positive X-axis, counterclockwise (1 and 2 quadrants) is -180° to 0, and clockwise (3 and 4 quadrants) is 0-180 °. And our doughnut, as we said at the beginning, has an infinite Angle, and of course, we’re dealing with 0 to 360 degrees, but even when we wrap it up, it still doesn’t match up with the minus 180 to 180 degrees that we calculated, so we’re going to have to do something with the Angle that we calculated.

So here I’m going to add 360 degrees when the Angle is less than 0. Some of you might ask me why I’m adding 360 degrees instead of 180 degrees.

This problem is very simple, because our doughnut shows the range of 180° ~ 360° in quadrant 1 and 2 after being converted to 0° ~ 360°, but the Angle we click on in quadrant 1 and 2 is -180° ~ 0. If 180 is added, it will be 0 ~ 180°, which still cannot satisfy our donut judgment. So in the case of less than 0, we add 360 degrees.

Then we search for the matching Angle in each section of doughnut according to the Angle and query until we find it.

We also need to be aware that there is a special case when we convert from 0 to 360, where a doughnut crosses the boundary between 0 and 360. Here’s an example:

Other parts, such as the animated implementation of the click, are computed and redrawn by the Animator. I won’t go into details here.

The text

Text drawing is relatively simple, we need to determine the position of the text is ok.

From the renderings, we know that the text is drawn with a guide line, and the text is either on the guide line, or under the guide line, or above and below.

To expand, I’ve given four literal attributes roughly:

  • The text is on the guide line
  • The text is under the guidance line
  • Text is on the guide line in quadrants 1 and 2, and below the guide line in quadrants 3 and 4
  • Align the text with the guide line

To calculate the position of the text, we need to determine the quadrant of the text, but all we know is the Angle at which the doughnut begins, the Angle at which the doughnut ends, and the radius of the doughnut, so we need to convert the Angle to the distance.

So what do we need to do? In fact, this is also a simple application of trigonometric function. According to the effect picture, the starting point of the text guide line is in the middle of the doughnut, so we can use the line between the center of the doughnut and the middle of a doughnut as the hypotenuse of the triangle, and calculate the coordinates of x and y according to the trigonometric function sin/cos.

By finding the starting point of the text guide line, we know which quadrant the text belongs to. We can simply determine x,y

Finally, for the drawing of text guide lines, we can use simple PathMeasure to make Path grow dynamically.

conclusion

As for donuts, the main difficulty of this project has been mentioned in this paper. Although the project is complicated, it is not very complicated, but I personally think it cannot be completed in a minute. In fact, there are still many details to be considered.

Of course, there is a lot of room for improvement in this library, such as the legend in the issue and the overlapping text when there is too much data (speaking of which, too much data is not suitable for pie charts…). .

But as long as I receive the issue, I won’t give up and keep iterating

More words will not say, welcome to read the project: github.com/razerdp/Ani…

I also recommend another project of mine: BasePopup: github.com/razerdp/Bas…

Welcome to the exchange, V-