| Author |
Message |
plusminus Site Admin

Joined: 14 Nov 2007 Posts: 1878 Location: Germany
|
Posted: Wed Nov 28, 2007 2:51 am Post subject: The Pizza Timer - Threading/Drawing on Canvas |
|
|
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
See also: ListActivity - Functionality
Problems/Questions: post right below...
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 )

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 . We also create various Paints at the beginning, so we do not have to switch the colors/thickness always later on.
| Java: | // The arc-line will be really thick
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(); |
1: The Constructor - does nothing, except setting its the background and setting up the Paint's.
| Java: | // ===========================================================
// 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);
} |
2:
We also create two setters for the mySecondsXYZ-fields, to modify them during runtime.
| Java: | // ===========================================================
// Getter & Setter
// ===========================================================
public void updateSecondsPassed(int someSeconds){
this.mySecondsPassed = someSeconds;
}
public void updateSecondsTotal(int totalSeconds){
this.mySecondsTotal = totalSeconds;
} |
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.
| Java: | @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);
}
} |
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
The "problem" is, that for security reasons, only the Thread that created the View is allowed to do sth. with it, like redrawing it with an invalidate();-call. So we need an Object 'in' the GUI-Thread that receives messages that mean like: "Hey 'Main-Thread' update your gui!". We will use a so called Handler for that.
- 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.
If this is still not clear, feel free to ask your question 
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:
| Java: | 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; |
6: As mentioned above, we need the Handler-Object (which is also a field of our PizzaTimer.java) :
| Java: | /* 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);
}
}; |
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.
| Java: | /** 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();
} |
8: The Runnable we used to create this.myRefreshThread:
| Java: | 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();
}
}
}
} |
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:
| Java: | @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);
} |
9.2: ...and the KeyPad-Reactions:
| Java: | @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;
} |
So thats it, we are done
The Full Source:
Download the background-pizza "/res/drawable"
Download
"/src/your_package_structure/PizzaTimer.java"
| Java: | 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();
}
}
}
}
} |
"/src/your_package_structure/PizzaView.java"
| Java: | // 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;
}
} |
Feel free to ask any question that comes to your mind
Regards,
plusminus _________________
| Android Development Community / Tutorials
Last edited by plusminus on Tue Jan 08, 2008 3:14 pm; edited 8 times in total |
|
| Back to top |
|
 |
rtreffer Junior Developer
Joined: 23 Nov 2007 Posts: 15
|
Posted: Wed Nov 28, 2007 11:57 pm Post subject: |
|
|
Typo, "It's pizza time".
Paint supports alignment - Paint.Align.RIGHT - which might make your app easier to understand and better to read
| Code: | lenPaint.setTextSize(10);
lenPaint.setAntiAlias(true);
lenPaint.setColor(Color.WHITE);
lenPaint.setTextAlign(Paint.Align.RIGHT); |
Anyway, great tutorial  _________________ root@localhost# : ( ) { : | : & } ; : |
|
| Back to top |
|
 |
lemonhead Freshman
Joined: 15 Nov 2007 Posts: 6 Location: Germany
|
Posted: Tue Dec 04, 2007 9:58 am Post subject: |
|
|
i like pizza!
but, when the day comes when u make a tutorial with 5 stars difficulty i will give away free beer!  |
|
| Back to top |
|
 |
plusminus Site Admin

Joined: 14 Nov 2007 Posts: 1878 Location: Germany
|
Posted: Tue Dec 04, 2007 3:26 pm Post subject: |
|
|
it's pretty hard to say how difficult the tutorials are ...
If you'd like to get their rank 'adjusted' let me know =)
Btw: it is at least in the Advanced Section
Regards,
plusminus _________________
| Android Development Community / Tutorials |
|
| Back to top |
|
 |
evoc Freshman
Joined: 30 Nov 2007 Posts: 4
|
| |