The
Timer - Threading/Drawing on Canvas
What is this: Having determined a huge market niche I decided to make the Pizza-Timer Killer-Application every one (or man) needs to survive
What this tutorial includes:
- Drawing on canvas'. Especially and Text.
- Threading (continuously update the GUI/PizzaCountdown)
- Correct Updating of the GUI/Views using a Handler
- Notifications
Difficulty: 2 of 5
What it will look like:
(During Cuntdown)

(Cuntdown Finished)

(Notification even when PizzaTimer was 'sent to background' (,BUT not yet killed
)


(Cuntdown Finished)

(Notification even when PizzaTimer was 'sent to background' (,BUT not yet killed

Description:
To accomplish our goal of creating the PizzaTimer we have to do the following things:
- Design our own Pizza-View, that has a pizza-picture as background and can draw Arcs and the time on its canvas.
- Creare a Timer-Thread, that 'ticks' every second.
- Update the PizzaView at every tick of the Timer-Thread
- Do some start/stop/settings stuff
0:So lets start with the PizzaView which is capable of displaying a Pizza as it background and do some painting on itself (its canvas).
At first, we set up the class-fields of PizzaView.java. Two int-values, one that takes the time, already passed and the other one takes the total time until the pizza is ready to be defeated
Using java Syntax Highlighting
- // The arc-line will be really thick <img src="http://www.anddev.org/images/smilies/icon_exclaim.gif" alt=":!:" title="Exclamation" />
- protected final int ARCSTROKEWIDTH = 20;
- // Set startup-values
- protected int mySecondsPassed = 0;
- protected int mySecondsTotal = 0;
- // Our Paint-ing-Device (Pen/Pencil/Brush/Whatever...)
- protected final Paint myArcSecondPaint = new Paint();
- protected final Paint myArcMinutePaint = new Paint();
- protected final Paint myCountDownTextPaint = new Paint();
- protected final Paint myPizzaTimeTextPaint = new Paint();
Parsed in 0.032 seconds, using GeSHi 1.0.8.4
1: The Constructor - does nothing, except setting its the background and setting up the Paint's.
Using java Syntax Highlighting
- // ===========================================================
- // Constructors
- // ===========================================================
- public PizzaView(Context context) {
- super(context);
- this.setBackground(getResources().getDrawable(R.drawable.pizza));
- // Black text for the countdown
- this.myCountDownTextPaint.setARGB(150, 0, 0, 0);
- this.myCountDownTextPaint.setTextSize(110);
- this.myCountDownTextPaint.setFakeBoldText(true);
- // Orange text for the IT PIZZA TIME
- this.myPizzaTimeTextPaint.setARGB(255, 255, 60, 10);
- this.myPizzaTimeTextPaint.setTextSize(110);
- this.myPizzaTimeTextPaint.setFakeBoldText(true);
- // Our arc fill be a lookthrough-red.
- this.myArcMinutePaint.setARGB(150, 170, 0, 0);
- this.myArcMinutePaint.setAntiAlias(true);
- this.myArcMinutePaint.setStyle(Style.STROKE);
- this.myArcMinutePaint.setStrokeWidth(ARCSTROKEWIDTH);
- this.myArcSecondPaint.setARGB(200, 255, 130, 20);
- this.myArcSecondPaint.setAntiAlias(true);
- this.myArcSecondPaint.setStyle(Style.STROKE);
- this.myArcSecondPaint.setStrokeWidth(ARCSTROKEWIDTH / 3);
- }
Parsed in 0.035 seconds, using GeSHi 1.0.8.4
2:
We also create two setters for the mySecondsXYZ-fields, to modify them during runtime.
Using java Syntax Highlighting
- // ===========================================================
- // Getter & Setter
- // ===========================================================
- public void updateSecondsPassed(int someSeconds){
- this.mySecondsPassed = someSeconds;
- }
- public void updateSecondsTotal(int totalSeconds){
- this.mySecondsTotal = totalSeconds;
- }
Parsed in 0.035 seconds, using GeSHi 1.0.8.4
3:The last thing to do in our PizzaView.java is obviously the onDraw(...)-Method, as we want to display a count-down. This is basically just some maths, like determining the percentage done and calculating that value against the 360°. It is basically like any ordinary analog clock. We also put the number of the minutes (if timeleft >= 60) or the seconds remaining (if timeleft < 60) to the middle of the screen.
Using java Syntax Highlighting
- @Override
- protected void onDraw(Canvas canvas) {
- /* Calculate the time left,
- * until our pizza is finished. */
- int secondsLeft = this.mySecondsTotal - this.mySecondsPassed;
- // Check if pizza is already done
- if(secondsLeft <= 0){
- /* Draw the "! PIZZA !"-String
- * to the middle of the screen */
- String itIsPizzaTime = getResources().getString(
- R.string.pizza_countdown_end);
- canvas.drawText(itIsPizzaTime,
- 10, (this.getHeight() / 2) + 30,
- this.myPizzaTimeTextPaint);
- }else{
- // At least one second left
- float angleAmountMinutes = ((this.mySecondsPassed * 1.0f)
- / this.mySecondsTotal)
- * 360;
- float angleAmountSeconds = ((60 -secondsLeft % 60) * 1.0f)
- / 60
- * 360;
- /* Calculate an Rectangle,
- * with some spacing to the edges */
- RectF arcRect = new RectF(ARCSTROKEWIDTH / 2,
- ARCSTROKEWIDTH / 2,
- this.getWidth() - ARCSTROKEWIDTH / 2,
- this.getHeight() - ARCSTROKEWIDTH / 2);
- // Draw the Minutes-Arc into that rectangle
- canvas.drawArc(arcRect, -90, angleAmountMinutes,
- this.myArcMinutePaint);
- // Draw the Seconds-Arc into that rectangle
- canvas.drawArc(arcRect, -90, angleAmountSeconds,
- this.myArcSecondPaint);
- String timeDisplayString;
- if(secondsLeft > 60) // Show minutes
- timeDisplayString = "" + (secondsLeft / 60);
- else // Show seconds when less than a minute
- timeDisplayString = "" + secondsLeft;
- // Draw the remaining time.
- canvas.drawText(timeDisplayString,
- this.getWidth() / 2 - (30 * timeDisplayString.length()),
- this.getHeight()/ 2 + 30,
- this.myCountDownTextPaint);
- }
- }
Parsed in 0.041 seconds, using GeSHi 1.0.8.4
The PizzaView.java is now completed
4:Lets get into the "time-managment logic" in the file "PizzaTimer.java".
Clearly the GUI needs to be update constantly(every second), as we want to see the time tick down
The way this done is not that ... "self understanding". I'll explain it a bit first and then everything should become clear with the picture below
- There will be a Thread, that sends a message that means "UPDATEYOURGUI" to the Handler.
- In the GUI-Thread, when the Handler receives the Message("UPDATEYOURGUI") it causes an invalidate(); to all participating Views.

5: Our actual PizzaTimer-Application does not contain much more than the things in the Picture
Lets first take a look at all the fields of PizzaTimer.java:
Using java Syntax Highlighting
- protected static final int DEFAULTSECONDS = 60 * 12; // 12 MInutes
- /* The value of these IDs is random!
- * they are just needed to be recognized */
- protected static final int SECONDPASSEDIDENTIFIER = 0x1337;
- protected static final int GUIUPDATEIDENTIFIER = 0x101;
- protected static final int PIZZA_NOTIFICATION_ID = 0x1991;
- /** is the countdown running at the moment ?*/
- protected boolean running = false;
- /** Seconds passed so far */
- protected int mySecondsPassed = 0;
- /** Seconds to be passed totally */
- protected int mySecondsTotal = DEFAULTSECONDS;
- /* Thread that sends a message
- * to the handler every second */
- Thread myRefreshThread = null;
- // One View is all that we see.
- PizzaView myPizzaView = null;
Parsed in 0.038 seconds, using GeSHi 1.0.8.4
6: As mentioned above, we need the Handler-Object (which is also a field of our PizzaTimer.java) :
Using java Syntax Highlighting
- /* The Handler that receives the messages
- * sent out by myRefreshThread every second */
- Handler myPizzaViewUpdateHandler = new Handler(){
- /** Gets called on every message that is received */
- // @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case PizzaTimer.SECONDPASSEDIDENTIFIER:
- // We identified the Message by its What-ID
- if (running) {
- // One second has passed
- mySecondsPassed++;
- if(mySecondsPassed == mySecondsTotal){
- // Time is finished, lets display a notification!
- // Get the notification manager serivce.
- NotificationManager nm = (NotificationManager)
- getSystemService(NOTIFICATION_SERVICE);
- /* The id we use here happens to be the
- * id of the text we display. You can use
- * any int here that is unique within
- * your application. */
- nm.notifyWithText(PIZZA_NOTIFICATION_ID,
- getText(R.string.pizza_notification_text),
- NotificationManager.LENGTH_LONG, null);
- }
- }
- // No break here --> runs into the next case
- case PizzaTimer.GUIUPDATEIDENTIFIER:
- // Redraw our Pizza !!
- myPizzaView.updateSecondsPassed(mySecondsPassed);
- myPizzaView.updateSecondsTotal(mySecondsTotal);
- myPizzaView.invalidate();
- break;
- }
- super.handleMessage(msg);
- }
- };
Parsed in 0.039 seconds, using GeSHi 1.0.8.4
7: Lets take a look at the onCreate-Activity.
It creates a new PizzaView and registers it as what we want to see.
It also creates a new Thread (secondCountDownRunner), that will send a message to the Handler we created above.
Using java Syntax Highlighting
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- this.myPizzaView = new PizzaView(this);
- this.myPizzaView.updateSecondsTotal(PizzaTimer.DEFAULTSECONDS);
- setContentView(this.myPizzaView);
- this.myRefreshThread = new Thread(new secondCountDownRunner());
- this.myRefreshThread.start();
- }
Parsed in 0.036 seconds, using GeSHi 1.0.8.4
8: The Runnable we used to create this.myRefreshThread:
Using java Syntax Highlighting
- class secondCountDownRunner implements Runnable{
- // @Override
- public void run() {
- while(!Thread.currentThread().isInterrupted()){
- Message m = new Message();
- m.what = PizzaTimer.SECONDPASSEDIDENTIFIER;
- PizzaTimer.this.myPizzaViewUpdateHandler.sendMessage(m);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- }
- }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4
9: And thats it, what is left are only a small menu, to reset the counter and the reactions on some Buttons (here the whole KeyPad):
9.1: The menu:
Using java Syntax Highlighting
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0,0,getResources().getString(R.string.menu_reset));
- return super.onCreateOptionsMenu(menu);
- }
- @Override
- public boolean onMenuItemSelected(int featureId, Item item) {
- switch(item.getId()){
- case 0:
- // Reset the counter and stop it
- this.mySecondsTotal = PizzaTimer.DEFAULTSECONDS;
- this.mySecondsPassed = 0;
- this.running = false;
- return true;
- }
- return super.onMenuItemSelected(featureId, item);
- }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4
9.2: ...and the KeyPad-Reactions:
Using java Syntax Highlighting
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- Message m = new Message();
- m.what = PizzaTimer.GUIUPDATEIDENTIFIER;
- switch(keyCode){
- case KeyEvent.KEYCODE_DPAD_UP:
- this.mySecondsTotal += 60; // One minute later
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- this.mySecondsTotal -= 60; // One minute earlier
- break;
- case KeyEvent.KEYCODE_DPAD_CENTER:
- this.running = !this.running; // START / PAUSE
- break;
- case KeyEvent.KEYCODE_DPAD_LEFT:
- this.mySecondsTotal += 1; // One second later
- break;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- this.mySecondsTotal -= 1; // One second earlier
- break;
- default:
- return super.onKeyDown(keyCode, event);
- }
- this.myPizzaViewUpdateHandler.sendMessage(m);
- return true;
- }
Parsed in 0.039 seconds, using GeSHi 1.0.8.4
So thats it, we are done
The Full Source:
Download the background-pizza "/res/drawable"
"/src/your_package_structure/PizzaTimer.java"
Using java Syntax Highlighting
- package org.anddev.android.pizzatimer;
- import android.app.Activity;
- import android.app.NotificationManager;
- import android.os.Bundle;
- import android.os.Handler;
- import android.os.Message;
- import android.view.KeyEvent;
- import android.view.Menu;
- import android.view.Menu.Item;
- public class PizzaTimer extends Activity {
- protected static final int DEFAULTSECONDS = 60 * 12; // 12 MInutes
- /* The value of these IDs is random!
- * they are just needed to be recognized */
- protected static final int SECONDPASSEDIDENTIFIER = 0x1337;
- protected static final int GUIUPDATEIDENTIFIER = 0x101;
- protected static final int PIZZA_NOTIFICATION_ID = 0x1991;
- /** is the countdown running at the moment ?*/
- protected boolean running = false;
- /** Seconds passed so far */
- protected int mySecondsPassed = 0;
- /** Seconds to be passed totally */
- protected int mySecondsTotal = DEFAULTSECONDS;
- /* Thread that sends a message
- * to the handler every second */
- Thread myRefreshThread = null;
- // One View is all that we see.
- PizzaView myPizzaView = null;
- /* The Handler that receives the messages
- * sent out by myRefreshThread every second */
- Handler myPizzaViewUpdateHandler = new Handler(){
- /** Gets called on every message that is received */
- // @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case PizzaTimer.SECONDPASSEDIDENTIFIER:
- // We identified the Message by its What-ID
- if (running) {
- // One second has passed
- mySecondsPassed++;
- if(mySecondsPassed == mySecondsTotal){
- // Time is finished, lets display a notification!
- // Get the notification manager serivce.
- NotificationManager nm = (NotificationManager)
- getSystemService(NOTIFICATION_SERVICE);
- /* The id we use here happens to be the
- * id of the text we display. You can use
- * any int here that is unique within
- * your application. */
- nm.notifyWithText(PIZZA_NOTIFICATION_ID,
- getText(R.string.pizza_notification_text),
- NotificationManager.LENGTH_LONG, null);
- }
- }
- // No break here --> runs into the next case
- case PizzaTimer.GUIUPDATEIDENTIFIER:
- // Redraw our Pizza !!
- myPizzaView.updateSecondsPassed(mySecondsPassed);
- myPizzaView.updateSecondsTotal(mySecondsTotal);
- myPizzaView.invalidate();
- break;
- }
- super.handleMessage(msg);
- }
- };
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- this.myPizzaView = new PizzaView(this);
- this.myPizzaView.updateSecondsTotal(PizzaTimer.DEFAULTSECONDS);
- setContentView(this.myPizzaView);
- this.myRefreshThread = new Thread(new secondCountDownRunner());
- this.myRefreshThread.start();
- }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- menu.add(0,0,getResources().getString(R.string.menu_reset));
- return super.onCreateOptionsMenu(menu);
- }
- @Override
- public boolean onMenuItemSelected(int featureId, Item item) {
- switch(item.getId()){
- case 0:
- // Reset the counter and stop it
- this.mySecondsTotal = PizzaTimer.DEFAULTSECONDS;
- this.mySecondsPassed = 0;
- this.running = false;
- return true;
- }
- return super.onMenuItemSelected(featureId, item);
- }
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- Message m = new Message();
- m.what = PizzaTimer.GUIUPDATEIDENTIFIER;
- switch(keyCode){
- case KeyEvent.KEYCODE_DPAD_UP:
- this.mySecondsTotal += 60; // One minute later
- break;
- case KeyEvent.KEYCODE_DPAD_DOWN:
- this.mySecondsTotal -= 60; // One minute earlier
- break;
- case KeyEvent.KEYCODE_DPAD_CENTER:
- this.running = !this.running; // START / PAUSE
- break;
- case KeyEvent.KEYCODE_DPAD_LEFT:
- this.mySecondsTotal += 1; // One second later
- break;
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- this.mySecondsTotal -= 1; // One second earlier
- break;
- default:
- return super.onKeyDown(keyCode, event);
- }
- this.myPizzaViewUpdateHandler.sendMessage(m);
- return true;
- }
- class secondCountDownRunner implements Runnable{
- // @Override
- public void run() {
- while(!Thread.currentThread().isInterrupted()){
- Message m = new Message();
- m.what = PizzaTimer.SECONDPASSEDIDENTIFIER;
- PizzaTimer.this.myPizzaViewUpdateHandler.sendMessage(m);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- }
- }
- }
Parsed in 0.054 seconds, using GeSHi 1.0.8.4
"/src/your_package_structure/PizzaView.java"
Using java Syntax Highlighting
- // Created by plusminus on 23:08:24 - 27.11.2007
- package org.anddev.android.pizzatimer;
- import android.content.Context;
- import android.graphics.Canvas;
- import android.graphics.Paint;
- import android.graphics.RectF;
- import android.graphics.Paint.Style;
- import android.view.View;
- public class PizzaView extends View{
- // ===========================================================
- // Fields
- // ===========================================================
- protected final int ARCSTROKEWIDTH = 20;
- // Set startup-values
- protected int mySecondsPassed = 0;
- protected int mySecondsTotal = 0;
- // Our Painting-Device (Pen/Pencil/Brush/Whatever...)
- protected final Paint myArcSecondPaint = new Paint();
- protected final Paint myArcMinutePaint = new Paint();
- protected final Paint myCountDownTextPaint = new Paint();
- protected final Paint myPizzaTimeTextPaint = new Paint();
- // ===========================================================
- // Constructors
- // ===========================================================
- public PizzaView(Context context) {
- super(context);
- this.setBackground(getResources().getDrawable(R.drawable.pizza));
- // Black text for the countdown
- this.myCountDownTextPaint.setARGB(150, 0, 0, 0);
- this.myCountDownTextPaint.setTextSize(110);
- this.myCountDownTextPaint.setFakeBoldText(true);
- // Orange text for the IT PIZZA TIME
- this.myPizzaTimeTextPaint.setARGB(255, 255, 60, 10);
- this.myPizzaTimeTextPaint.setTextSize(110);
- this.myPizzaTimeTextPaint.setFakeBoldText(true);
- // Our minute-arc-paint fill be a lookthrough-red.
- this.myArcMinutePaint.setARGB(150, 170, 0, 0);
- this.myArcMinutePaint.setAntiAlias(true);
- this.myArcMinutePaint.setStyle(Style.STROKE);
- this.myArcMinutePaint.setStrokeWidth(ARCSTROKEWIDTH);
- // Our minute-arc-paint fill be a less lookthrough-orange.
- this.myArcSecondPaint.setARGB(200, 255, 130, 20);
- this.myArcSecondPaint.setAntiAlias(true);
- this.myArcSecondPaint.setStyle(Style.STROKE);
- this.myArcSecondPaint.setStrokeWidth(ARCSTROKEWIDTH / 3);
- }
- // ===========================================================
- // onXYZ(...) - Methods
- // ===========================================================
- @Override
- protected void onDraw(Canvas canvas) {
- /* Calculate the time left,
- * until our pizza is finished. */
- int secondsLeft = this.mySecondsTotal - this.mySecondsPassed;
- // Check if pizza is already done
- if(secondsLeft <= 0){
- /* Draw the "! PIZZA !"-String
- * to the middle of the screen */
- String itIsPizzaTime = getResources().getString(
- R.string.pizza_countdown_end);
- canvas.drawText(itIsPizzaTime,
- 10, (this.getHeight() / 2) + 30,
- this.myPizzaTimeTextPaint);
- }else{
- // At least one second left
- float angleAmountMinutes = ((this.mySecondsPassed * 1.0f)
- / this.mySecondsTotal)
- * 360;
- float angleAmountSeconds = ((60 -secondsLeft % 60) * 1.0f)
- / 60
- * 360;
- /* Calculate an Rectangle,
- * with some spacing to the edges */
- RectF arcRect = new RectF(ARCSTROKEWIDTH / 2,
- ARCSTROKEWIDTH / 2,
- this.getWidth() - ARCSTROKEWIDTH / 2,
- this.getHeight() - ARCSTROKEWIDTH / 2);
- // Draw the Minutes-Arc into that rectangle
- canvas.drawArc(arcRect, -90, angleAmountMinutes,
- this.myArcMinutePaint);
- // Draw the Seconds-Arc into that rectangle
- canvas.drawArc(arcRect, -90, angleAmountSeconds,
- this.myArcSecondPaint);
- String timeDisplayString;
- if(secondsLeft > 60) // Show minutes
- timeDisplayString = "" + (secondsLeft / 60);
- else // Show seconds when less than a minute
- timeDisplayString = "" + secondsLeft;
- // Draw the remaining time.
- canvas.drawText(timeDisplayString,
- this.getWidth() / 2 - (30 * timeDisplayString.length()),
- this.getHeight()/ 2 + 30,
- this.myCountDownTextPaint);
- }
- }
- // ===========================================================
- // Getter & Setter
- // ===========================================================
- public void updateSecondsPassed(int someSeconds){
- this.mySecondsPassed = someSeconds;
- }
- public void updateSecondsTotal(int totalSeconds){
- this.mySecondsTotal = totalSeconds;
- }
- }
Parsed in 0.052 seconds, using GeSHi 1.0.8.4
Feel free to ask any question that comes to your mind
Regards,
plusminus








it's pretty hard to say how difficult the tutorials are ...



