Android Canvas | How to Draw 2D Donut Chart?

android-draw-donut-chartThis post provides some tips and tricks of how to use Android canvas to draw non-trivial shapes.  We will create a donut chart, which is a common type of chart, as a custom view. Some basics about how to create custom view is covered in  Android Simple Custom View.

( 1 ) Create Custom View “DonutChart.java”

package com.hmkcode.views;


import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;

import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.View;

public class DonutChart extends View {

	
	 private float radius;
	
	 Paint paint;
	 Paint shadowPaint;
	 
	 Path myPath;
	 Path shadowPath;
	 
	 RectF outterCircle;
	 RectF innerCircle;
	 RectF shadowRectF;
	 
	public DonutChart(Context context, AttributeSet attrs) {
		super(context, attrs);
		
		TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.DonutChart,
                0, 0
        );
		
		try {
            radius = a.getDimension(R.styleable.DonutChart_radius, 20.0f);
        } finally {
            a.recycle();
        }
		
		paint = new Paint();
		paint.setDither(true);       
		paint.setStyle(Paint.Style.FILL); 
		paint.setStrokeJoin(Paint.Join.ROUND); 
		paint.setStrokeCap(Paint.Cap.ROUND);     
		paint.setAntiAlias(true); 
		paint.setStrokeWidth(radius / 14.0f);
		
		shadowPaint = new Paint();
	    shadowPaint.setColor(0xf0000000);
	    shadowPaint.setStyle(Paint.Style.STROKE); 
	    shadowPaint.setAntiAlias(true);
	    shadowPaint.setStrokeWidth(6.0f);
	    shadowPaint.setMaskFilter(new BlurMaskFilter(4, BlurMaskFilter.Blur.SOLID));


		myPath = new Path();
		shadowPath = new Path();


		outterCircle = new RectF();
		innerCircle = new RectF();
		shadowRectF = new RectF();

		float adjust = (.019f*radius);
		shadowRectF.set(adjust, adjust, radius*2-adjust, radius*2-adjust);
		
		adjust = .038f * radius;
		outterCircle.set(adjust, adjust, radius*2-adjust, radius*2-adjust);
		
		adjust = .276f * radius;
		innerCircle.set(adjust, adjust, radius*2-adjust, radius*2-adjust);

	}

	
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		// draw shadow
		paint.setShader(null);
		float adjust = (.0095f*radius);
		paint.setShadowLayer(8, adjust, -adjust, 0xaa000000);
		drawDonut(canvas,paint, 0,359.9f);
		
		
		// green	
		setGradient(0xff84BC3D,0xff5B8829); 
		drawDonut(canvas,paint, 0,60);		
		
		//red
		setGradient(0xffe04a2f,0xffB7161B); 
		drawDonut(canvas,paint, 60,60);
		
		// blue
		setGradient(0xff4AB6C1,0xff2182AD);
		drawDonut(canvas,paint, 120,60);
		
		// yellow
		setGradient(0xffFFFF00,0xfffed325);
		drawDonut(canvas,paint, 180,180);
	

		
	}
	
	public void drawDonut(Canvas canvas, Paint paint, float start,float sweep){

		myPath.reset();
		myPath.arcTo(outterCircle, start, sweep, false);
		myPath.arcTo(innerCircle, start+sweep, -sweep, false);
		myPath.close();
		canvas.drawPath(myPath, paint);
	}
	
	public void setGradient(int sColor, int eColor){
		paint.setShader(new RadialGradient(radius, radius, radius-5, 
				new int[]{sColor,eColor}, 
				new float[]{.6f,.95f},TileMode.CLAMP) );
	}
	
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		
	    int desiredWidth = (int) radius*2;
	    int desiredHeight = (int) radius*2;

	    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
	    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
	    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
	    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

	    int width;
	    int height;

	    //70dp exact
	    if (widthMode == MeasureSpec.EXACTLY) {
	        width = widthSize;
	    }else if (widthMode == MeasureSpec.AT_MOST) {
		    //wrap content
	        width = Math.min(desiredWidth, widthSize);
	    } else {
	        width = desiredWidth;
	    }

	    //Measure Height
	    if (heightMode == MeasureSpec.EXACTLY) {
	        height = heightSize;
	    } else if (heightMode == MeasureSpec.AT_MOST) {
	        height = Math.min(desiredHeight, heightSize);
	    } else {
	        height = desiredHeight;
	    }

	    //MUST CALL THIS
	    setMeasuredDimension(width, height);
	}
	
}

( 2 ) Defining Custom Attribute

<resources>
    <declare-styleable name="DonutChart">
        <attr name="radius" format="dimension"/>
    </declare-styleable>
</resources>

( 3 ) Adding DonutChart View to Layout

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:customviews="http://schemas.android.com/apk/res/com.hmkcode.views"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.hmkcode.views.MainActivity" 
    android:orientation="vertical">

 	<com.hmkcode.views.DonutChart
            android:id="@+id/donutChart"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            customviews:radius="90dp"
           android:layout_gravity="center"
         />

</LinearLayout>

Source Code @ GitHub

Leave a Reply