diff options
Diffstat (limited to 'docs/html/resources/articles/timed-ui-updates.jd')
-rw-r--r-- | docs/html/resources/articles/timed-ui-updates.jd | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/docs/html/resources/articles/timed-ui-updates.jd b/docs/html/resources/articles/timed-ui-updates.jd new file mode 100644 index 0000000..863387c --- /dev/null +++ b/docs/html/resources/articles/timed-ui-updates.jd @@ -0,0 +1,149 @@ +page.title=Updating the UI from a Timer +@jd:body + +<img style="margin: 1.5em; float: right;" src="images/JFlubber.png" alt="" id="BLOGGER_PHOTO_ID_5135098660116808706" border="0"> + +<p><strong>Background</strong>: While developing my first useful +(though small) application for Android, which was a port of an existing +utility I use when podcasting, I needed a way of updating a clock +displayed on the UI at regular intervals, but in a lightweight and CPU +efficient way.</p> + +<p><strong>Problem</strong>: In the original application I used +java.util.Timer to update the clock, but that class is not such a good +choice on Android. Using a Timer introduces a new thread into the +application for a relatively minor reason. Thinking in terms of mobile +applications often means re-considering choices that you might make +differently for a desktop application with relatively richer resources +at its disposal. We would like to find a more efficient way of updating +that clock.</p> + +<p><strong>The Application</strong>: The original application is a +Java Swing and SE application. It is like a stopwatch with a lap timer +that we use when recording podcasts; when you start the recording, you +start the stopwatch. Then for every mistake that someone makes, you hit +the flub button. At the end you can save out the bookmarked mistakes +which can be loaded into the wonderful +<a href="http://audacity.sourceforge.net/" title="Audacity">Audacity</a> +audio editor as a labels track. You can then see where all of the mistakes +are in the recording and edit them out.</p> + +<p>The article describing it is: <a href="http://www.developer.com/java/ent/print.php/3589961" title="http://www.developer.com/java/ent/print.php/3589961">http://www.developer.com/java/ent/print.php/3589961</a></p> + +<p>In the original version, the timer code looked like this:</p> + +<pre>class UpdateTimeTask extends TimerTask { + public void run() { + long millis = System.currentTimeMillis() - startTime; + int seconds = (int) (millis / 1000); + int minutes = seconds / 60; + seconds = seconds % 60; + + timeLabel.setText(String.format("%d:%02d", minutes, seconds)); + } +}</pre><p>And in the event listener to start this update, the following Timer() instance is used: +</p><pre>if(startTime == 0L) { + startTime = evt.getWhen(); + timer = new Timer(); + timer.schedule(new UpdateTimeTask(), 100, 200); +}</pre> + +<p>In particular, note the 100, 200 parameters. The first parameter +means wait 100 ms before running the clock update task the first time. +The second means repeat every 200ms after that, until stopped. 200 ms +should not be too noticeable if the second resolution happens to fall +close to or on the update. If the resolution was a second, you could +find the clock sometimes not updating for close to 2 seconds, or +possibly skipping a second in the counting, it would look odd).</p> + +<p>When I ported the application to use the Android SDKs, this code +actually compiled in Eclipse, but failed with a runtime error because +the Timer() class was not available at runtime (fortunately, this was +easy to figure out from the error messages). On a related note, the +String.format method was also not available, so the eventual solution +uses a quick hack to format the seconds nicely as you will see.</p> + +<p>Fortunately, the role of Timer can be replaced by the +android.os.Handler class, with a few tweaks. To set it up from an event +listener:</p> + +<pre>private Handler mHandler = new Handler(); + +... + +OnClickListener mStartListener = new OnClickListener() { + public void onClick(View v) { + if (mStartTime == 0L) { + mStartTime = System.currentTimeMillis(); + mHandler.removeCallbacks(mUpdateTimeTask); + mHandler.postDelayed(mUpdateTimeTask, 100); + } + } +};</pre> + +<p>A couple of things to take note of here. First, the event doesn't +have a .getWhen() method on it, which we handily used to set the start +time for the timer. Instead, we grab the System.currentTimeMillis(). +Also, the Handler.postDelayed() method only takes one time parameter, +it doesn't have a "repeating" field. In this case we are saying to the +Handler "call mUpdateTimeTask() after 100ms", a sort of fire and forget +one time shot. We also remove any existing callbacks to the handler +before adding the new handler, to make absolutely sure we don't get +more callback events than we want.</p> + +<p>But we want it to repeat, until we tell it to stop. To do this, just +put another postDelayed at the tail of the mUpdateTimeTask run() +method. Note also that Handler requires an implementation of Runnable, +so we change mUpdateTimeTask to implement that rather than extending +TimerTask. The new clock updater, with all these changes, looks like +this:</p> + +<pre>private Runnable mUpdateTimeTask = new Runnable() { + public void run() { + final long start = mStartTime; + long millis = SystemClock.uptimeMillis() - start; + int seconds = (int) (millis / 1000); + int minutes = seconds / 60; + seconds = seconds % 60; + + if (seconds < 10) { + mTimeLabel.setText("" + minutes + ":0" + seconds); + } else { + mTimeLabel.setText("" + minutes + ":" + seconds); + } + + mHandler.postAtTime(this, + start + (((minutes * 60) + seconds + 1) * 1000)); + } +};</pre> + +<p>and can be defined as a class member field.</p> + +<p>The if statement is just a way to make sure the label is set to +10:06 instead of 10:6 when the seconds modulo 60 are less than 10 +(hopefully String.format() will eventually be available). At the end of +the clock update, the task sets up another call to itself from the +Handler, but instead of a hand-wavy 200ms before the update, we can +schedule it to happen at a particular wall-clock time — the line: start ++ (((minutes * 60) + seconds + 1) * 1000) does this.</p> + +<p>All we need now is a way to stop the timer when the stop button +is pressed. Another button listener defined like this:</p> + +<pre>OnClickListener mStopListener = new OnClickListener() { + public void onClick(View v) { + mHandler.removeCallbacks(mUpdateTimeTask); + } +};</pre> + +<p>will make sure that the next callback is removed when the stop button +is pressed, thus interrupting the tail iteration.</p> + +<p>Handler is actually a better choice than Timer for another reason +too. The Handler runs the update code as a part of your main thread, +avoiding the overhead of a second thread and also making for easy +access to the View hierarchy used for the user interface. Just remember +to keep such tasks small and light to avoid slowing down the user +experience.</p> + + |