Thursday, June 2, 2011

Android, the countdown timer and the progress bar

In an effort to make my program more game-like, I'm interested in adding a countdown timer to the question page.

Let's google it.

Ok, here's a link: http://developer.android.com/reference/android/os/CountDownTimer.html

And some code:

new CountdownTimer(30000, 1000) {

public void onTick(long millisUntilFinished) {
mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
}

public void onFinish() {
mTextField.setText("done!");
}
}.start();


So, what I really need to do is combine this with a kind of progress bar, as the async dialog did. But, it has to be contained in a little box on the bottom of the layout. Or displayed as a graphic. Let's google again.

Hmmm...here's a link that looks somewhat promising:

http://stackoverflow.com/questions/5113960/showing-time-on-progressbar-in-android


bar = (ProgressBar) findViewById(R.id.progress);
bar.setProgress(total);
int twoMin = 2 * 60 * 1000; // 2 minutes in milli seconds

/** CountDownTimer starts with 2 minutes and every onTick is 1 second */
cdt = new CountDownTimer(twoMin, 1000) {

public void onTick(long millisUntilFinished) {

total = (int) ((dTotal / 120) * 100);
bar.setProgress(total);
}

public void onFinish() {
// DO something when 2 minutes is up
}
}.start();


So, there must be a UI widget called progressbar? That's cool.

http://developer.android.com/reference/android/widget/ProgressBar.html

google progress bar example...

android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.ProgressBar.Small"
android:layout_marginRight="5dp" />

to make it a scroll bar, use this:

style="@android:style/Widget.ProgressBar.Horizontal"

android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_marginRight="5dp" />

Let's add it and see what it looks like:

Ok, it's a little thing on the bottom. Three won't be room for add, though, anymore. First, let's figure out how to make it work.

Well, I put it in between the question and the choices. But, let's make it work. Why is it so short? Let's try making width "fill_parent".

Ok, that makes it as wide as the display, although right now it's a clashing grey color. Ok, now, let's put the above piece of timer into effect:


bar = (ProgressBar) findViewById(R.id.progress);
bar.setProgress(total);
int twoMin = 2 * 60 * 1000; // 2 minutes in milli seconds

/** CountDownTimer starts with 2 minutes and every onTick is 1 second */
cdt = new CountDownTimer(twoMin, 1000) {

public void onTick(long millisUntilFinished) {

total = (int) ((dTotal / 120) * 100);
bar.setProgress(total);
}

public void onFinish() {
// DO something when 2 minutes is up
}
}.start();



Ok, I get it. it works on a scale of 1 to 100, where 100 fill out the orange. Since it's called every millisecond (per the 1000 parameter in the constructor, and lasts for one minute (the first constructor parameter - I changed it), you have to manipulate that into a number which fits onto the 100 scale and times it such that the 100 is reached at the end of whatever time frame you want. So, if you have 1 minute, it's 60 * 1000 milliseconds. So, you need to take that number and divide it by the actual milliseconds to get the fraction of your time frame that the current milliseconds covers. Then multiply that by 100 to get it from the 0 to 1 range to the 0 to 100 range.

Hmm...I'm having some trouble here.

The first parm is total time, the second one is countdown interval.

So, if I do this,

new CountDownTimer(100 * 1000, 1000) {

it will cover 100 seconds. So each tick is 1 second. Let's test that:

m_bar.setProgress ( m_total++ );

where m_total starts at zero, and it works.

Now, let's convert that to using remaining milliseconds

So, secondsRemaing = millisUntilFinished / 1000

If we start out with 100 seconds total time, and we want to increase the bar, seconds remaining 100 - secondsRemaining.

int secondsRemaining = (int) millisUntilFinished / 1000;
m_bar.setProgress ( 100 - secondsRemaining);

But, actually, it looks just as good to decrease the bar, and simplifies things.

int secondsRemaining = (int) millisUntilFinished / 1000;
m_bar.setProgress ( secondsRemaining);

But, now we need to scale it to say 10 seconds.

So, let's say total time is 10 seconds:

new CountDownTimer(10 * 1000, 1000) {

We need to scale the calculation to cover the 100 of the progress bar, because now it will only decrease from 10.

Let's test that theory.

Right. So, now let's multiply it by 10.


int secondsRemaining = (int) millisUntilFinished / 1000;
m_bar.setProgress ( secondsRemaining * 20 - 1);

Anyway, I would like to generalize the calculation as a fraction of 100. Really, what I want to do is, if you have x amount of milliseconds remaining, and your total time is y, then your fraction is x/y * 100.

Let's try this:

m_bar.setProgress ( secondsRemaining / (10 * 1000) * 100 );

No good. Zero movement on the bar.

How about debugging:

int secondsRemaining = (int) millisUntilFinished / 1000;
Log.d(TAG, ">>>>>>>>> secondsRemaining: " + secondsRemaining);

float fraction = secondsRemaining / (10 * 1000);
Log.d(TAG, ">>>>>>>>> fraction: " + fraction);

It shows the fraction is zero. Because I'm mixing up milliseconds with seconds.

Let's change it to this:


int secondsRemaining = (int) millisUntilFinished / 1000;
Log.d(TAG, ">>>>>>>>> secondsRemaining: " + secondsRemaining);

float fraction = millisUntilFinished / (10 * 1000);
Log.d(TAG, ">>>>>>>>> fraction: " + fraction);

Nope. Still zeroes.

Wait. Is it converting to int an truncating?

int secondsRemaining = (int) millisUntilFinished / 1000;
Log.d(TAG, ">>>>>>>>> secondsRemaining: " + secondsRemaining);

float fraction = millisUntilFinished / (float) (10 * 1000);
Log.d(TAG, ">>>>>>>>> fraction: " + fraction);

Ah, that was it. Java converts the division to an int-based calculation, so it truncates it down to zero.

Now, let's try this:

m_bar.setProgress ( (int) fraction * 100 );

No - it's still forcing fraction to 0. The int took precedence. Let's try this:

m_bar.setProgress ( (int) (fraction * 100) );

Will the fraction * 100 force the fraction to an int and make it a zero? It did for the divide, right?

No, ti's ok. The problem is that on a *divide*, if it's an int, it *truncates*( the result. On multiplication, where it's expanding the significant digits, you don't lose any digits, so it has no impact. Wow.

It's still leaving some orange on the bar when it's complete, like 10% The last call is at one second, so, the fraction is around .19, so that make 19 out of 100. We need to make it more granular. Since we've gone to a more generalized equation which involve calculating the ratio of time passed instead of the time units, seconds, we can now change the granularity.

new CountDownTimer(10 * 1000, 100)

Ahh, much better. It advances in smaller increments, and only leave a tiny bit at the end.

Just out of curiosity:

new CountDownTimer(10 * 1000, 10)

It doesn't seem to make any difference.

Ok, we have a wrap.

Here's the final routine:

private void startCountdownTimer() {

m_bar = (ProgressBar) findViewById(R.id.progress_bar);

m_bar.setProgress(m_total);

final int totalMsecs = 10 * 1000; // 10 seconds in milli seconds
int callInterval = 100;

/** CountDownTimer */
new CountDownTimer(totalMsecs, callInterval) {

public void onTick(long millisUntilFinished) {

int secondsRemaining = (int) millisUntilFinished / 1000;

float fraction = millisUntilFinished / (float) totalMsecs;

// progress bar is based on scale of 1 to 100;
m_bar.setProgress ( (int) (fraction * 100) );
}

public void onFinish() {
Log.d(TAG, ">>>>>>>>> countdown timer on finish");
}
}.start();

3 comments: