Android Custom Views: Creating an Animated Pie Chart from Scratch (Part 2 of 4)

by

Introduction to Part 2

In Part 1 of this series, we covered the basics of custom views, including: deciding if a custom view is the best solution to your problem, the three basic methods for creating a custom view, and the required constructors you’ll need to implement when subclassing the View class.

In Part 2, we’ll dive into creating our PieChart class and adding its essential elements. We’ll be creating three classes:

  • PieSlice: A model class to hold property information on individual slices of the pie
  • PieData: A class to help add and remove data from the pie chart
  • PieChart: The class that will be extending View

The PieSlice class

The PieSlice class will act as the data model for the individual pie slices in the chart. It will contain all the necessary information for PieChart to accurately display its data. Here’s what it looks like:

As you can see above, the PieSlice class contains six variables:

  • name: The data label that will appear next to the pie slice
  • value: The data value that pie slice represents
  • startAngle: The angle in the unit circle that the arc of the pie slice will begin at
  • sweepAngle: The number of degrees the arc of the pie slice will travel
  • indicatorCircleLocation: The position of the marker on each pie slice, which will be connected to name and value labels with a straight line
  • paint: The paint used to draw the pie slice

The PieData class

This class will contain formatted data for our PieChart view. Using a PieData object will ensure that PieChart always has a consistent type of data coming in, and will allow users to easily assign, add, and delete data from the pie chart.

A few additional notes on PieData:

  • The pieSlices hashmap is used to store the PieSlice objects that will populate the PieChart. The hashmap allows us to add to existing slices, and ensures that there won’t be any duplicates when it comes time to paint the chart.
  • The private function createPaint assigns a paint color at random if the user has not specified one.
  • The variable totalValue keeps track of the total value to be associated with our PieChart. This information will be vital to calculating the dimensions of our pie slices.

The PieChart class

Finally, it’s time for the real meat of the view! First, let’s take care of the constructors we discussed in Part 1. Our PieChart class is pretty simple, with no custom xml properties, so we can use the Kotlin trick demonstrated in Part 1:

Next, let’s define some variables to keep track of our data object. We’ll also create a few Paint() objects that we’ll use to draw the pie chart, followed by an init { } block to set their properties. The Kotlin function apply makes quick work of this:

Here’s how we’ll use each of the Paint() objects we just defined:

  • borderPaint will be used to paint the lines that divide the pie slices
  • indicatorCirclePaint will be used to paint markers on each pie slice, which will be connected to name and value labels with a straight line
  • indicatorCircleRadius will be used to set the radius of the markers on each pie slice
  • linePaint will be used to paint the straight lines connecting the markers on each pie slice to their associated name and value labels
  • mainTextPaint will be used to paint the pie slice name labels
  • oval will be used to define the bounds of the complete pie chart circle

The code above also defines our data variable, and sets it to null. To populate that variable, we need to create a method that takes in a PieData object and assigns it to data. While we’re at it, let’s also calculate the dimensions of each pie slice. We’ll do this by creating two new methods: fun setData(data: PieData)and fun setPieSliceDimensions():

The setData() function is pretty self-explanatory. It sets our data variable to the data being passed in, calls our setPieDimensions() method (more on this below), and invalidates the view. But how do we figure out the coordinates of our indicator circles? Well…remember that time you were doing trigonometry homework and thought “I’m never going to use this!” ? Time to use it!

Calculating the PieSlice dimensions – Let’s get math-y!

We can think of the pie chart as a unit circle, and our pie slices as arcs on that circle. As you might recall from your trigonometry days, a unit circle goes from 0˚ to 360˚, so we know we’ll need to scale all of our values to that range.

We start by setting a lastAngle variable to 0f — this is the degree at which the last slice ended and the next slice begins. We then calculate the sweepAngle, and use both the startAngle and sweepAngle to determine the coordinates for the indicator marker. The details of the algorithm are as follows:

  1. Set setLastAngle to 0f
  2. Loop through all slices:
    • Set the startAngle of the current slice to the end of the last
    • Calculate sweepAngle(currentValue / totalValueOfPieChart) * 360f
    • Add this sweepAngle to lastAngle so we know where to start the next slice
    • Find the middle angle of the current pie slice to calculate where the indicator circle will be placed: (sweepAngle / 2) + startAngle
    • [[@Sid: I don’t see this part reflected in the code]] Calculate the distance the indicator circle will be placed from the center of the pie chart (I chose a distance of 3/8ths of the pie chart distance from the edge, but you can choose whatever distance you’d like): (3 * layoutParams.height / 8)
    • Calculate indicatorCircleLocation.x and indicatorCircleLocation.y using some basic trigonometry:
      • indicatorCircleLocation.x = distance * Math.cos(Math.toRadians(middleAngle.toDouble())).toFloat() + width / 2
      • indicatorCircleLocation.y = distance * Math.sin(Math.toRadians(middleAngle.toDouble())).toFloat() + layoutParams.height / 2

Sizing and resizing view elements

Before we can draw our pie chart, we need a way to determine how much space each element should take up on our screen. The following elements need to have their sizes set when we create the view, and will need to be updated every time the view is resized:

  • Bounds for the complete pie chart circle
  • Thickness of the indicator lines, dots, and borders
  • Size of text for the labels
  • Dimensions of the pie slices, including the locations of their indicators

We’ll base all of our sizes based on the height of our view, which will ensure that the text and graphics are readable on various devices and resolutions.

The diameter of our pie chart circle will be the full height of our view, and it will be centered along the width:

Note that we assigned these values as defaults, meaning if we want to set custom bounds for our circle at any point, we have the ability to do so.

Next, we’ll create a quick method to calculate the text, border, line, and indicator circle sizes:

Finally, we need to put these methods to use somewhere! We want them to fire when the view is first created, and then again whenever it’s resized. Luckily for us, there’s a method we can override that does exactly what we need:

Overriding onDraw()

Let’s take a look at the function we’ll use to draw the pie chart. Our final onDraw() will look like this:

Pretty simple — we’re just iterating through all of the entries in our pie slice data and drawing them onto the screen, along with their indicators.

onDraw()conveniently offers a canvas object with a multitude of methods. We’re using the drawArc() method twice — once to draw each pie slice with its respective paint, and then again to draw the white borders around the slices. Lastly, we draw the indicators — let’s take a look at that implementation:

The dimensions of an indicator line depend on the position of its associated slice, which determines the line’s location relative to the pie chart. For example, a slice on the right side of the chart should have an indicator line that points to the right, while one on the left should have an indicator line that points left. Let’s create a simple enum to keep track of these directions:

All we need to do now is pass the canvas, pie slice data, and indicator alignment to drawIndicatorLine() and drawIndicatorText(). Let’s take a look at those methods:

These methods are doing essentially the same thing — one with a line, and the other with text. We calculate the xOffset based on whether the pie slice is on the left or right side of the pie chart, and use it to determine the x coordinate for the line or text. drawIndicatorText() has one additional step — setting the text alignment to either left or right, depending on the side of the pie chart on which the text is drawn.

Let’s use our PieChart view!

If you’ve been following along, you should have a fully functioning pie chart view that’s ready to be used. So, let’s use it!

Head over to a layout file (I’ll be adding the view to MainActivity, so my layout file is activity_main.xml) and add it to the view:

Next, we’ll need to create a PieData object and populate its data, using the add method we created earlier:

Here we declared and initialized a PieData object as a property variable of MainActivity and added values to it in onCreate(). Notice that even though we didn’t give Dave a color, he was dynamically assigned one when the chart was created.

What’s next?

Part 3 of this series will cover touch events and interacting with the PieChart view. Stay tuned!

Leave a Reply

Your email address will not be published. Required fields are marked