Introduction
Even though they've been around for a long time, I was looking at an analog clock the other day and noticed something that I had not paid attention to before. On a clock with three hands (i.e., hour, minute, second), as the second hand made its way around the clock face, the minute hand would move ever so slightly, rather than jump, from one minute to the next. And likewise for the hour hand, albeit way more slowly. So, for example, if the second hand was at :30, the minute hand would be halfway between one minute and the next. If the minute hand was at :15, the hour hand would be 1/4 of the way between one hour and the next. Hmmmmm.......
Background
While this is my first Android article here at CodeProject, it's not the first app I've created. That number would be in the 40s, with two of them actually being published, and a third one not far behind. This is, however, the first app I've created where I had to add code to do the "drawing" of some UI control rather than letting the framework do the drawing for me. That said, this article will be a brief demonstration of extending the ProgressBar
class.
Using the code
What I wanted my progress bar extension to do was to display its value as text, either on or below the progress bar. Given that requirement, the name of the new class became:
public class TextProgressBar extends ProgressBar
Without overriding anything, the class can be referenced in the layout file like:
<com.dcrow.BarClock.TextProgressBar android:id="@+id/progHour"
android:layout_height="64dp"
android:layout_marginbottom="25dp"
android:layout_width="match_parent"
android:max="23">
At this point, the progress bar would behave normally. Since I'm wanting a few customizations, the onDraw()
method will need to be overridden. This is where the actual drawing will take place. Calling the superclass onDraw()
will take care of drawing the actual progress bar, while everything else (e.g., tick marks, text) can be drawn afterward.
The first thing I wanted was a horizontal line in the middle of, and running the length of, the progress bar:
int nMiddle = getHeight() / 2;
canvas.drawLine(0, nMiddle, getWidth(), nMiddle, m_paintLine);
The next thing I wanted was a series of vertical tick marks along the previously drawn horizontal line. Depending on the maximum value of the progress bar, the number of tick marks and the spacing between them will vary.
float increment = (float) getWidth() / getMax();
for (int i = 0; i <= getMax(); i++)
{
float x = i * increment;
canvas.drawLine(x, nMiddle - 20.0f, x, nMiddle + 20.0f, m_paintLine);
}
Looks good, but the tick marks in the minute and second progress bars are sort of hard to read. A solution for those two would be to make every 5th tick mark bigger. Analog clock faces coincide with this in that the numbers 1-12 are each 5 seconds apart. We would just need to check the value of the loop control variable i
to see if it is evenly divisible by 5, like:
if (i % 5 == 0)
canvas.drawLine(x, nMiddle - 30.0f, x, nMiddle + 30.0f, m_paintLine);
else
canvas.drawLine(x, nMiddle - 20.0f, x, nMiddle + 20.0f, m_paintLine);
Now that the tick marks are in place, it's time for the text (i.e., the progress bar's value). I want it to be centered on the progress line and move along with it from left to right. On the far left and right edges, I do not want it to disappear.
float fPercent = ((float) getProgress() / getMax()) * getWidth();
m_paintText.getTextBounds(m_text, 0, m_text.length(), m_bounds);
float pos = fPercent;
if (pos < m_bounds.width() / 2.0f)
{
m_paintText.setTextAlign(Align.LEFT);
pos = 0.0f;
}
else if (pos > getWidth() - (m_bounds.width() / 2.0f))
{
m_paintText.setTextAlign(Align.RIGHT);
pos = getWidth();
}
else
m_paintText.setTextAlign(Align.CENTER);
canvas.drawText(m_text, pos, getHeight() - 10.0f, m_paintText);
With the text-drawing code in place, the progress bars now look like:
Now that the control code itself was (mostly) done, I needed to respond to time changes and update the progress bars accordingly. I set a timer for 1-second intervals, and when the time changes, the following code executes:
Calendar now = Calendar.getInstance();
int nHour = now.get(Calendar.HOUR_OF_DAY);
int nMinute = now.get(Calendar.MINUTE);
int nSecond = now.get(Calendar.SECOND);
m_progHour.setProgress(nHour);
m_progMinute.setProgress(nMinute);
m_progSecond.setProgress(nSecond);
Points of Interest
Initially, the main layout looked similar to the following:
<linearlayout android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<textview android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/hour"
android:textsize="20sp">
<com.dcrow.barclock.textprogressbar android:id="@+id/progHour"
android:layout_height="64dp"
android:layout_width="match_parent"
android:max="23">
<textview android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/minute"
android:textsize="20sp">
<com.dcrow.barclock.textprogressbar android:id="@+id/progMinute"
android:layout_height="64dp"
android:layout_width="match_parent"
android:max="59">
<textview android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/second"
android:textsize="20sp">
<com.dcrow.barclock.textprogressbar android:id="@+id/progSecond"
android:layout_height="64dp"
android:layout_width="match_parent"
android:max="59">
</LinearLayout>
This worked as intended but it did not give the hour and minute progress bars enough granularity. Each bar's progress advanced as a whole unit rather than as a percentage of the whole. To correct this, I simply needed to change each bar's max
value to 86399 (0-based number of seconds per day) and 3599 (0-based number of seconds per hour) respectively. Now since each bar was basing its value on total number of seconds, I needed to change the value sent to each bar's setProgress()
method, like:
m_progHour.setProgress((nHour * 3600) + (nMinute * 60) + nSecond);
m_progMinute.setProgress((nMinute * 60) + nSecond);
m_progSecond.setProgress(nSecond);
The final result:
Notice that since the second progress bar is at 32 seconds, the minute progress bar is roughly 1/2 between two minute intervals. Similarly, since the minute progress bar is at 49 minutes, the hour progress bar is slightly over 3/4 between two hour intervals. That's the look I set out to create.
Colors
Partway through the creation of this app, I decided that gradient colors for the progress bar and its background would be just enough to make it "less boring." The first solution I came upon involved using the progress bar's android:progressDrawable
property in the layout file. Assigning that to the following file in the drawable folder did the trick:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="3dp" />
<gradient android:startColor="#f0f0f0"
android:centerY="1.0"
android:endColor="#b9b9b9"
android:angle="270" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="3dp" />
<gradient android:startColor="#93d1ea"
android:centerY="1.0"
android:endColor="#35aad7"
android:angle="270" />
</shape>
</clip>
</item>
</layer-list>
With these colors, it gave the progress bar a blue gradient and its background a gray gradient, both going from top to bottom. After looking at it a few times, I thought it would be better if the colors were customizable. So I went down that road...
I needed to find a way, at runtime, to get this drawable, modify it, and put it back in place. After working on it for a few days using everything I could find, I came up nothing. No matter what code I put in place, the android:progressDrawable
property took precedence. I then decided to forgo the use of that property and do it all via code.
Working backward from the progress bar's setProgressDrawable()
method, I found that there were several drawable objects that needed to be created and tied together in order for this to work. Because the progress bar effectively uses three drawables (i.e., background, secondary progress, progress) all stacked on top of each other to achieve its effect, I needed to send setProgressDrawable()
a LayerDrawable
object. That object would contain the other drawables.
The drawable used for the background looks like:
GradientDrawable gdBackground =
new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[] {colors[0], colors[1]});
gdBackground.setShape(GradientDrawable.RECTANGLE);
gdBackground.setCornerRadius(7.0f);
and the drawable used for the secondary progress and the (primary) progress looks like:
GradientDrawable gdProgress =
new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[] {colors[2], colors[3]});
gdProgress.setShape(GradientDrawable.RECTANGLE);
gdProgress.setCornerRadius(7.0f);
ClipDrawable cdProgress = new ClipDrawable(gdProgress, Gravity.LEFT, ClipDrawable.HORIZONTAL);
Putting these into an array and letting the LayerDrawable
object know the ordering looks like:
Drawable[] drawables = {gdBackground, cdProgress, cdProgress};
LayerDrawable ld = new LayerDrawable(drawables);
ld.setId(0, android.R.id.background);
ld.setId(1, android.R.id.secondaryProgress);
ld.setId(2, android.R.id.progress);
bar.setProgressDrawable(ld);
Since I needed this to happen for each of the (four) progress bars, I wrapped it all up in a changeColors(TextProgressBar bar, int[] colors)
method belonging to the activity. I can call that method in the activity's onCreate()
method, or someplace else where the colors get changed (e.g., preference).
Enjoy!