Skip to content

Progress Bar Custom View

Eric Petzel edited this page May 3, 2015 · 29 revisions

Overview

Objective

We're going to create our own implementation of a determinate progress bar. The progress bar will have an indicator that represents a goal. We will define custom attributes, handle measuring and drawing, and animations.

Prepare app skeleton

Clone this repository, and import the project in Android Studio. Build and run the app to see the following screen:

The skeleton app contains a GoalProgressBar class which extends Android's ProgressBar, but doesn't add any additional functionality. Clicking on the Reset Progress button will update the progress to a new value, which will help us test our changes during development.

Initialization

We won't need to extend android's ProgressBar as we will be drawing the progress manually. Change GoalProgressBar to extend View instead of ProgressBar, and remove the style and android:indeterminite attributes from our custom view's definition in activity_main.xml.

In order to draw the progress we'll need to use a Paint object. Define a new paint object that is initialized after view creation:

private Paint progressPaint;

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

private void init() {
    progressPaint = new Paint();
    progressPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}

It's important that the Paint is only created once, as reinitializing the object each time it is used in drawing would affect performance.

Our progress bar will have an indicator for a given 'goal'. The goal indicator can appear at any given position on the progress bar, and will have an adjustable size.

Create public setters for an int progress, as well as an int goal. When either of these methods are called, we'll update a boolean isGoalReached method that we'll use to determine which color to draw the progress bar.

public void setProgress(int progress) {
  this.progress = progress;
  updateGoalReached();
  invalidate();
}

public void setGoal(int goal) {
  this.goal = goal;
  updateGoalReached();
  invalidate();
}

private void updateGoalReached() {
  isGoalReached = progress >= goal;
}

Custom Attributes

We're going to create custom attributes to allow users to customize different components of our progress bar. To start out, we'll define member variables for each customizable attribute:

// height of the goal indicator 
private float goalIndicatorHeight;

// thickness of the goal indicator
private float goalIndicatorThickness;

// bar color when the goal has been reached
private int goalReachedColor;

// bar color when the goal has not been reached
private int goalNotReachedColor;

// bar color for the unfilled section (remaining progress)
private int unfilledSectionColor;

// thickness of the progress bar
private float barThickness;

In order to be able to set these variables from Java code, create setters for these member variables. When any of these setters are called, be sure to call postInvalidate(); after updating the field so the view is redrawn.

In order to set these variables from XML, we first have to declare a styleable resource in res/values/attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="GoalProgressBar">
    <attr name="goalIndicatorHeight" format="dimension"/>
    <attr name="goalIndicatorThickness" format="dimension"/>
    <attr name="goalReachedColor" format="color"/>
    <attr name="goalNotReachedColor" format="color"/>
    <attr name="unfilledSectionColor" format="color"/>
    <attr name="barThickness" format="dimension"/>
  </declare-styleable>
</resources>

The variable names and defined <attr names do not have to be the same, but are doing so here for the sake of consistency.

Next we'll add these new attributes to our existing GoalProgressBar in activity_main.xml. Before we can start defining custom attributes, we need to add an additional XML namespace on the root element: xmlns:app="http://schemas.android.com/apk/res-auto"

Once the new app namespace is defined (we can call it anything, it doesn't have to be app) we can start adding these attributes to our GoalProgressBar:

<com.codepath.customprogressbar.GoalProgressBar
    android:id="@+id/progressBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:goalReachedColor="@color/green"
    app:goalNotReachedColor="@color/dark_gray"
    app:unfilledSectionColor="@color/gray"
    app:barThickness="4dp"
    app:goalIndicatorHeight="16dp"
    app:goalIndicatorThickness="4dp"/>

Modify init to include an AttributeSet as a parameter. It is from this object that we'll extract the attribute values.

To extract the attributes from the AttributeSet, we'll use a TypedArray. A TypedArray is basically a container to hold onto defined attributes. This object must be recycled once we're done using it, so we'll interact with it in a try block so we can be sure to call recycle() on it once we're finished:

TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.GoalProgressBar, 0, 0);
try {
  // extract attributes to member variables from typedArray
} finally {
  typedArray.recycle(); 
}

Extract each of our defined attributes from the typed array by providing the styleable ID to the typedArray. Be sure to set each member variable via it's setter for consistent behavior:

setGoalReachedColor(a.getColor(R.styleable.GoalProgressBar_goalReachedColor, Color.BLUE));

Measuring

Now that we have the framework in place for setting the required fields, we can implement the measuring logic. To do this we will override onMeasure, so we can tell our parent view what size we'd like the view to be.

For the width of the view, we can use whatever width imposed by our parent (provided in widthMeasureSpec), but we'll add some custom handling for determining our height.

Since the tallest component of our view will be the goal indicator, we'll use it's height to determine the height of the entire view:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  // set width
  int width = MeasureSpec.getSize(widthMeasureSpec);

  // set height
  int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  int height;
  switch (MeasureSpec.getMode(heightMeasureSpec)) {
    case MeasureSpec.EXACTLY:
      // we must be exactly the given size 
      height = heightSize;
      break;
    case MeasureSpec.AT_MOST:
      // we can not be bigger than the specified height
      height = (int) Math.min(goalIndicatorHeight, heightSize);
      break;
    default:
      // we can be whatever height want
      height = (int) goalIndicatorHeight;
      break;
  }

  setMeasuredDimension(width, height);
}

This will properly handle the sizing of our view, even when we allow a customizable sized goal indicator.

Drawing the view

Now our view has set it's appropriate height and width, we're ready to draw the content of the view to the screen. We do this by overriding onDraw.

onDraw provides a Canvas onto which we can use our Paint to draw. We'll start with three different components to draw, so we'll have three separate calls to draw onto the Canvas.

We need to draw the section of the progress bar that is 'filled' - so if the maximum is set to 100, and progress is set to 70, we'll the filled section will be drawn to 7/10 the width. The color of this section will depend on whether or not progress meets/exceeds our goal.

We'll also have to draw the remaining/unfilled section of the progress bar, which in the case of 70% progress would be 3/10 of the width, starting where the filled section left off.

We also have to draw the 'goal' indicator, which will be at a position defined by the user.

@Override
protected void onDraw(Canvas canvas) {
  int halfHeight = getHeight() / 2;
  int progressEndX = (int) (getWidth() * progress / 100f);

  // draw the part of the bar that's filled
  progressPaint.setStrokeWidth(barThickness);
  progressPaint.setColor(isGoalReached ? goalReachedColor : goalNotReachedColor);
  canvas.drawLine(0, halfHeight, progressEndX, halfHeight, progressPaint);

  // draw the unfilled section
  progressPaint.setColor(unfilledSectionColor);
  canvas.drawLine(progressEndX, halfHeight, getWidth(), halfHeight, progressPaint);

  // draw the goal indicator
  float indicatorPosition = getWidth() * goal / 100f;
  progressPaint.setColor(goalReachedColor);
  progressPaint.setStrokeWidth(goalIndicatorThickness);
  canvas.drawLine(indicatorPosition, halfHeight - goalIndicatorHeight / 2,
                indicatorPosition, halfHeight + goalIndicatorHeight / 2, progressPaint);
}

Finding these guides helpful?

We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.

Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.

Clone this wiki locally