Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile

Circular Progress Bar in Android

5.00/5 (5 votes)
19 Oct 2020CPOL9 min read 13.5K   187  
This article describes the concept of a custom view and implements a circular progress bar.
The article explains designing the custom view in Android along with some useful backgrounds in this regard. Then, a circular progress bar is developed that can be used in different apps as an example custom view.

Introduction

The Android framework provides several default views that are responsible for layouting, measuring, and drawing themselves and their related children. The views can also be responsible to save their states and manipulating their UI properties. Android UI elements are all based on the View class that is a base class for these objects. Some of them are built-in views involving "Button" and "TextView".

Sometimes, it is highly needed to create a custom view (your own View) based on the app's requirements. Customizing a view needs some related backgrounds that are presented in this article. Customizing a view deals with some primary different aspects that start from drawing and ends at storing. These different aspects should be simultaneously exploited to develop a well-behavior custom view with appropriate interactions. These aspects can be briefly listed as given below:

  • Drawing
  • Interactions
  • Measurement
  • Layouting
  • Attributing

The above aspects are fully presented in the Background chapter of this article. Understanding the concepts of these aspects can answer a lot of questions that may arise through developing a custom view such as a circular progress bar.

Figure 1: Two instances of Circular Progress Bar

A circular progress bar is a type of progress bar with a circular shape that can be used in some specific apps. Two implemented versions of these progress bars are shown in Figure 1.

The developed progress bar can be easily implemented in other apps that is fully explained in this article.

Background

As stated earlier in the Introduction section, customizing a view requires some primary aspects that are respectively mentioned in this section. Before presenting these aspects, the following points are highly needed to be mentioned.

First of all, showing a simple view on the screen is hierarchy such as the root node is firstly shown, then its children are traversed, in-orderly. It means view parents are showed before their corresponding view children on the screen. It should be noted that this screen visualization is enough fast such that you cannot detect this in-order traversing.

All views should know how to draw, measure, and layout themselves for both built-in and custom ones. These methods are individually and independently called to show a view on the screen. Drawing, measuring, and layouting are the most important parts of designing a custom view that are totally defined and explained in this section.

A custom view class can be easily defined as given below:

Java
public class myCustomView extends View {    

    public myCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

The above code defines an empty custom view whose parts should be defined respectively. In the sequel, the primary aspects and their implementation details are presented one after another.

Drawing

The rendering of the view on the screen is controlled in this part. Overriding the onDraw() method enables us to depict the whole visual parts of the view. This method exploits a Canvas object to paint the visual shape of the view such as an instance custom view. For example, you can draw a circle, line, and arc by this method which is the visual parts of your own custom view by this method. It should be noted that this method is similarly evaluated for all views including the custom or built-in ones.

Canvas object is a simple 2D rendering tool for drawing shapes that can be used as given in the following code block to draw a simple circle:

Java
@Override
protected void onDraw(Canvas canvas){
    Paint paint=new Paint();
    paint.setColor(Color.BLACK);
    canvas.drawCircle(centerX,centerY,radius,paint);    
}

The above code draws a black circle around the specific center and radius. The paint object depicts some styles of the drawing procedure involving the color, font size and so on. The canvas object is able to draw a set of simple shapes whose list can be found by a simple net searching.

Measuring

Measuring handles the dimensions of the view involving its width and height. This part is evaluated by the onMeasure() method that should be overridden. This method determines the width and height (the dimension) of a view based on its contents and parent's constraints. This method has two integer input arguments which are the measured specific width and height, respectively.

The overriding method makes a trade-off between the desired and specific (forced) dimensions. The desired dimension is the one that is given in the design procedure (manually designed in the XML files) while the specific (forced) dimension is given by the input arguments of the method. It must be emphasized that different modes exist that are presented in the following:

  • Exact Mode: In this mode, the view is forced to have a specific dimension. The view must have the forced dimension that may happen through considering the match_parent attribute.
  • At_Most Mode: This mode happens when the maximum size is needed to be considered. In this mode, the view cannot be larger than the forced dimension.
  • Unspecified Mode: The view is free to have the desired dimension and the specific width and heights values are not set in this mode. This mode happens when both width and height are set to be wrap_content.

For better understanding, a simple form of this method is given in the following:

Java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

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

    int width, height;

    if (widthMode == MeasureSpec.EXACTLY) {
        width = widthMeasureSpec;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        width = Math.min(desiredWidth, widthMeasureSpec);
    } else {
        width = desiredWidth;
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightMeasureSpec;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        height = Math.min(desiredHeight, heightMeasureSpec);
    } else {
        height = desiredHeight;
    }

    setMeasuredDimension(width, height);
}

As can be seen, the width and height of the view are manipulated based on the desired and forced dimensions and the pre-defined modes. The method onMeasuredDimension() finally is called with the final dimensions. Note that, this method must be called at the end of the method to determine the dimensions of the view.

It is worth mentioning that there is another method resovelSizeAndState() that simplifies the above code which is given in the following code block:

Java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int width = resolveSizeAndState(desiredWidth, widthMeasureSpec);
    int height= resolveSizeAndState(desiredHeight, heightMeasureSpec);

    setMeasuredDimension(width, height);
}

About the above code, it should be noted that the padding distances must be considered for correctly resizing. Up to now, the measuring aspect is totally clear by the above-mentioned notes.

Layouting

This part determines the exact location of the view in the screen that is evaluated by overriding the onLayout() method. This method has five input arguments that are the changed (boolean), left, top, right, and bottom (integer) values, respectively. Though layouting, the position of the view can be manipulated to exactly locate at the best position. The first argument (changed) determines any change in the containing layout of this view that is a boolean variable.

Assume a custom view is developed that is around a center with specific values. Let centerX and centerY be the positions of the custom view. The following code can be used for this special case (such as used in the developed circular progress bar):

Java
@Override
protected void onLayout(Boolean changed, int left, int top, int right, int bottom) {
    
   if(changed){
    centerX=(left+right)/2;
    centerY=(top+bottom)/2;
   }
}

Clearly, the center of the custom view locates at the center of the layout.

Attributing

A custom view surely contains a set of style and format attributes that should be accurately implemented. These XML attributes are optimally determined to follow the custom view configurations. To define these XML attributes, the following resources must be inserted within res/values/attrs.xml such as given below. In this example code, the custom view is named by myCustomView whose required attributes are considered.

XML
<code><?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="myCustumView">
       <attr name="backColor" format="Color />
       <attr name="textSize format="Integer" />
       ...
   </declare-styleable>
</resources></code>

The above XML enables us to define some required attributes such as colors, sizes, and so on. These attributes can be modified in both static and dynamic cases. In the static case, the values of these attributes can be initialized via setting their values in the layout XML as given in the following code:

XML
<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"        
        tools:context="com.example.mycustomview.MainActivity">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal|center_vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_weight="1">

            <com.example.mycustomview
                android:layout_width="400dp"
                android:layout_height="400dp"
                android:id="@+id/pb1"
                app:backColor="#aaaaaa"                
                app:textSize="20"/>

        </LinearLayout>       
        </LinearLayout>
    </RelativeLayout>

Indeed, the pre-defined attributes can be forced to have some initial values as defined in the above code.

In the dynamic case, the attributes of the custom view are modified by the code as presented in the circular progress bar that is attached in this article.

Interactions

A standard view reacts to its programmed events such as Touch, Click, and so on. For each definable event, the custom view can be programmed to interact appropriately in which some of its attributes may change. For this purpose, one instant of the custom view is defined in the main activity and its event listeners are implemented to perform some specific tasks.

For example, the developed circular progress bar is supposed to interact to the click event. The progress bar's value is increased by 5 after each click event. This example is implemented as follows:

Java
public class MainActivity extends AppCompatActivity {
    public ProgressBar progressBar1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar1=(ProgressBar)findViewById(R.id.pb1);
        progressBar2=(ProgressBar)findViewById(R.id.pb2);
        progressBar1.setValue(80);
        progressBar2.setValue(30);
        progressBar1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (progressBar1.value<100) {
                    progressBar1.value+=5;
                    progressBar1.Invalidate();
                }else{
                    progressBar1.value=0;
                }
            }
        });       
    }
}

As can be seen, the progress bar interacts to the click event and its value increases. Therefore, the amount of the progress value that is visually shown must be accurately modified and the view should be repaint (reshown). For this purpose, the Invalidate() method is called that tells the view that some of its attributes are changed.

In fact, the view or its parent's attributes may be required to be reshown in this situation. Some methods are employed to tell the Android framework about this situation which are listed below that their goal is to awake view about its invalidation:

  • Invalidate(): This method is called when the view is needed to be redraw. It will result in onDraw being called instantly. This method must be called on an UI-thread.
  • Postinvalidate(): This method is the same of the Invalidate() method except it is called on a background thread.
  • requestLayout(): The changes that may affect the size should be followed by calling this method. This method eventually trigger both onMeasure() and onLayout() methods not only for the mentioned view but also for the whole children in the parent views.

It is very important to properly call one of the above methods according to the occur event (change). It is mainly because these methods have different complexities that are not needed to be evaluated in some special cases. Typically, one of the above methods is surely called in the interaction part.

Circular Progress Bar

Based on the defined aspects, the circular progress bar is developed through the following codes. First of all, the view is attributed as given below:

Java
package com.example.mycustomview;

import android.content.Context;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;

public class ProgressBar extends View {

    private int backColor=0;
    private int frontColor=0;
    private int lineColor=0;
    private boolean displayValue;
    private int outerCirlceRadius=200;
    private int innerCirlceRadius=150;
    private int displayTextSize=20;
    private int value=90;
    private int centerX=200;
    private int centerY=200;
    private int width=400;
    private int height=400;
    private String name="";

    public ProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupAttrs(attrs);
    }
    private void setupAttrs(AttributeSet attrs){

        try {
            for(int i=0;i<attrs.getAttributeCount();i++) {
                if(attrs.getAttributeName(i).contains("backColor")) {
                    backColor = attrs.getAttributeIntValue(i, backColor);
                }else if(attrs.getAttributeName(i).contains("frontColor")){
                    frontColor = attrs.getAttributeIntValue(i, frontColor);
                }else if(attrs.getAttributeName(i).contains("lineColor")){
                    lineColor = attrs.getAttributeIntValue(i, lineColor);
                }else if(attrs.getAttributeName(i).contains("displayValue")){
                    displayValue = attrs.getAttributeBooleanValue(i, false);
                }else if(attrs.getAttributeName(i).contains("name")){
                    name = attrs.getAttributeValue(i);
                }else if(attrs.getAttributeName(i).contains("layout_width")) {
                    String text=attrs.getAttributeValue(i);
                           text=text.substring(0,text.length()-5);
                    width = Integer.parseInt(text);
                }else if(attrs.getAttributeName(i).contains("layout_height")) {
                    String text=attrs.getAttributeValue(i);
                           text=text.substring(0,text.length()-5);
                    height = Integer.parseInt(text);
                }
            }
        }catch(Exception ex) {
            Toast.makeText(getContext(),ex.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onLayout(boolean changed,int left,int top,int right,int bottom){
        if(changed){
            centerX=(left+right)/2;
            centerY=(top+bottom)/2;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        width = Math.min(width, widthMeasureSpec);
        height = Math.min(height, heightMeasureSpec);
        width=Math.min(width,height);
        height=Math.min(width,height);
        innerCirlceRadius=(35*width)/100;
        outerCirlceRadius=width/2;
        displayTextSize=width/10;
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas){
        Paint paint=new Paint();
        paint.setColor(backColor);
        canvas.drawCircle(centerX,centerY,outerCirlceRadius,paint);
        paint.setColor(frontColor);
        RectF rectf=new RectF();
        rectf.left=centerX-outerCirlceRadius;rectf.top=centerY-outerCirlceRadius;
        rectf.right=centerX+outerCirlceRadius;
        rectf.bottom=centerY+outerCirlceRadius;
        if(value<=50) {
            canvas.drawArc(rectf, 180f, ((float)value)*3.6f, true, paint);
        }else{
            canvas.drawArc(rectf, 180f, 180f, true, paint);
            canvas.drawArc(rectf, 0f, (value-50)*3.6f, true, paint);
        }
        paint.setColor(Color.WHITE);
        canvas.drawCircle(centerX,centerY,innerCirlceRadius,paint);
        if(displayValue) {
            paint.setColor(lineColor);
            paint.setTextSize(displayTextSize);
            String displayText=name + " = "+ value;
            int len=displayText.length()*displayTextSize/4;
            canvas.drawText(displayText, centerX-len, centerY, paint);
        }
    }

    public void setValue(int value){
        this.value=value;
        invalidate();
    }
    public int getValue(){
        return this.value;
    }
}

Different parts of circular progress bar such as measuring, layouting, and drawing are specially described in the above code. Then, the main activity code is given here:

Java
package com.example.mycustomview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {
    public ProgressBar progressBar1;
    public ProgressBar progressBar2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar1=(ProgressBar)findViewById(R.id.pb1);
        progressBar2=(ProgressBar)findViewById(R.id.pb2);
        progressBar1.setValue(80);
        progressBar2.setValue(30);
        progressBar1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (progressBar1.getValue()<100) {
                    progressBar1.setValue(progressBar1.getValue() + 5);
                }else{
                    progressBar1.setValue(0);
                }
            }
        });
        progressBar2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (progressBar2.getValue()<100) {
                    progressBar2.setValue(progressBar2.getValue() + 5);
                }else{
                    progressBar2.setValue(0);
                }
            }
        });
    }
}

The main activity's layout of the project is given below:

XML
<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        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.example.mycustomview.MainActivity">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal|center_vertical"
            android:weightSum="2">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_weight="1">

            <com.example.mycustomview.ProgressBar
                android:layout_width="400dp"
                android:layout_height="400dp"
                android:id="@+id/pb1"
                app:backColor="#aaaaaa"
                app:frontColor="#444444"
                app:lineColor="#000000"
                app:displayValue="true"
                app:name="PB1"/>

        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_weight="1">

            <com.example.mycustomview.ProgressBar
                android:layout_width="400dp"
                android:layout_height="400dp"
                android:id="@+id/pb2"
                app:backColor="#aa00aa"
                app:frontColor="#440044"
                app:lineColor="#000000"
                app:displayValue="true"
                app:name="PB2"/>

        </LinearLayout>
        </LinearLayout>
    </RelativeLayout>

Finally, the XML attributes of the circular progress bar is presented in the following:

XML
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ProgressBar">
        <attr name="backColor" format="color"/>
        <attr name="frontColor" format="color"/>
        <attr name="lineColor" format="color"/>
        <attr name="displayValue" format="boolean"/>
        <attr name="name" format="string"/>
    </declare-styleable>
</resources>

The result is shown in Figure 2:

Figure 2: Two implemented instances of Circular Progress Bar

The developed progress bar can be simply used in the other projects that are explained in the next section.

Using the Code

To use the developed circular progress bar, it suffices to insert the circular progress bar to your XML main activity's layout.

Points of Interest

This article discuses about developing a custom view and its related aspects and also provides a circular progress bar for better understanding of the mentioned concepts.

History

  • 19th October, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)