Smarter ideas worth writing about.

Android Circle Progress Indicator

Background

The Android Circle Progress Indicator is the brother project to the iOS CircleProgressView Control described in a previous blog post by Eric Rolf. To reiterate, we were in need of a way to represent a wide range of data being displayed in a cross platform mobile application.

Solution

Working in unison, our designers and developers created the Circle Progress Indicator (CPI).  This version of the CPI was developed for the Android platform. The sample project was developed in Android Studio, and can be downloaded from our GitHub:

Link: https://github.com/CardinalNow/Android-CircleProgressIndicator 

Technical Details

Note: This section requires a working knowledge of developing for Android.

Curved Text Label

Working with the CPI is fairly straightforward.   The first piece we need to look at is the CurvedTextView object.  It’s a custom extension of the Android TextView widget, and can be found in the root package of the example app. The big takeaway here is extending TextView gives us access to the overridden onDraw method.  Here is where we do the calculations to best display the text centered on the ring.  Using the screen size, length of the text in the box, offsets, and radius of our circle we create the arc and draw it to the screen:

@Override
protected void onDraw(Canvas canvas) {
int centerXOnView = getWidth() / 2;
	int centerYOnView = getHeight() / 2;

	int viewXCenterOnScreen = getLeft() + centerXOnView;
	int viewYCenterOnScreen = getTop() + centerYOnView;

float threeDpPad = getResources().getDimension(R.dimen.three_dp);
	float rad = getResources().getDimension(R.dimen.seventy_dp);

	int leftOffset = (int) (viewXCenterOnScreen - (rad + (threeDpPad * 4)));
	int topOffset = (int) (viewYCenterOnScreen - (rad + (threeDpPad * 3)));
	int rightOffset = (int) (viewXCenterOnScreen + (rad + (threeDpPad * 4)));
	int bottomOffset = (int) (viewYCenterOnScreen + (rad + threeDpPad));

	RectF oval = new RectF(leftOffset, topOffset, rightOffset, bottomOffset);

	int textLength = getText().length();
	if ((textLength % 2) != 0) {
		textLength = textLength + 1;
	}
	this.myArc.addArc(oval, -90 - (textLength * 2), 90 + textLength + 10);

	canvas.drawTextOnPath((String) getText(), this.myArc, 0, 10, this.mPaintText);
	invalidate();
}	

When you declare your custom text view in your activity’s layout, ensure you call it by the name you gave it, including package. For our example app that looks like this:

<com.cardinalsolutions.progressindicator.CurvedTextView
android:id="@+id/compliance_curved_text"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:gravity="center"
	android:layout_alignParentTop="true"
	android:textSize="@dimen/fifteen_sp"
	android:text="@string/curved_text_value"
	android:textStyle="bold"
	android:paddingTop="@dimen/ten_dp" />

If you are going to set the text on your new widget dynamically, you’ll need to declare in in your activity as well. Make sure you declare your new custom object (not the default TextView):

private CurvedTextView mCurvedTextView;


mCurvedTextView.setText(Dynamic Text);

Custom Rings The foreground and background rings are defined in res/drawable/circle_progress_foreground.xml & res/drawable/circle_progress_background.xml respectively. The drawables provide the developer the ability to modify the CIP size, color, ring thickness, etc., by tweaking the attributes. For example, the foreground circle looks like this:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@android:id/progress">
            <shape
                android:innerRadius="@dimen/sixty_dp"
                android:shape="ring"
                android:thickness="@dimen/seven_dp">
                
                <gradient
                    android:startColor="@color/dark_blue"
                    android:endColor="@color/coral_blue"
                    android:type="sweep" />   
            </shape>
    </item>
</layer-list>

The tag is where the magic happens. A brief explanation of each attribute inside that tag:

// innerRadius defines the size of the inside of the ring in dp
android:innerRadius="@dimen/sixty_dp"
// shape defines the shape, in this case it’s a ring
android:shape="ring"
// thickness defines the thickness of the progress bar around the circle
android:thickness="@dimen/seven_dp">
// startColor is the beginning color of the ring
android:startColor="@color/dark_blue"
// endColor is the end color of the progress bar, regarless of length
android:endColor="@color/coral_blue"
// type is the type of gradient.  The operation system handles the transition of the sweep
android:type="sweep"

To add a CPI to your activity, you need to add two ProgressBar widgets to your activity’s layout.xml. Using a relative layout, place the ProgressBar widgets on top of each other:

<ProgressBar
	style="?android:attr/progressBarStyleHorizontal"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:layout_centerInParent="true"
	android:indeterminate="false"
	android:max="100"
	android:progress="100"	android:progressDrawable="@drawable/circle_progress_background" />

<ProgressBar
	android:id="@+id/circle_progress_bar"
	style="?android:attr/progressBarStyleHorizontal"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:layout_centerInParent="true"
	android:max="100"
	android:rotation="-90"
	android:indeterminate="false"
	android:progressDrawable="@drawable/circle_progress_foreground" />

These are standard ProgressBar widgets, with nothing outside the ordinary (refer to the developer site for specifics on ProgressBar). The only customization we do here is using our new res drawable backgrounds on the ProgressBar widgets. The first one is set to background, and the second is set to foreground. For the example app we wanted to progress to start at the 0 point on the circle, so the rotation tag is set to -90:

// first widget
android:progressDrawable="@drawable/circle_progress_background"

// second widget
android:progressDrawable="@drawable/circle_progress_foreground"
android:rotation="-90"

The foreground progress circle needs to be declared in your activity, and then you can set it with whatever value you’d like:

private ProgressBar mProgress;


mProgress.setProgress(65);

Happy Coding!

Share:

About The Author

Shane is a Principal Developer II in Cardinal's Cincinnati office who is currently focused on mobile applications development and Azure mobile back end services (NodeJS).