andbook!.pdf - Learning Android Get an anddev.org - Android-Shirt Back to index
anddev.org Header Logo
FAQ Search Top rated articles Browse Feeds anddev.org - Authors Contact Details Register Log in

The Pizza Timer - Threading/Drawing on Canvas

Goto page 1, 2  Next
 
       anddev.org - Android Development Community | Android Tutorials | Index -> Advanced Tutorials
Author Message
plusminus
Site Admin


Joined: 14 Nov 2007
Posts: 1878
Location: Germany

PostPosted: Wed Nov 28, 2007 2:51 am    Post subject: The Pizza Timer - Threading/Drawing on Canvas Reply with quote

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 Exclamation

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

Question 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 Exclamation)


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 Rolling Eyes . 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 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();


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 Exclamation
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 Exclamation
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 Exclamation
Idea 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.


Arrow If this is still not clear, feel free to ask your question Exclamation


5: Our actual PizzaTimer-Application does not contain much more than the things in the Picture Exclamation

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 Exclamation

The Full Source:
Download the background-pizza "/res/drawable"
Arrow 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 Exclamation

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
View user's profile Send private message Send e-mail Visit poster's website
rtreffer
Junior Developer


Joined: 23 Nov 2007
Posts: 15

PostPosted: Wed Nov 28, 2007 11:57 pm    Post subject: Reply with quote

Typo, "It's pizza time".
Paint supports alignment - Paint.Align.RIGHT - which might make your app easier to understand and better to read Smile

Code:
      lenPaint.setTextSize(10);
      lenPaint.setAntiAlias(true);
      lenPaint.setColor(Color.WHITE);
      lenPaint.setTextAlign(Paint.Align.RIGHT);


Anyway, great tutorial Smile

_________________
root@localhost# : ( ) { : | : & } ; :
Back to top
View user's profile Send private message
lemonhead
Freshman


Joined: 15 Nov 2007
Posts: 6
Location: Germany

PostPosted: Tue Dec 04, 2007 9:58 am    Post subject: Reply with quote

i like pizza! Very Happy

but, when the day comes when u make a tutorial with 5 stars difficulty i will give away free beer! Surprised
Back to top
View user's profile Send private message
plusminus
Site Admin


Joined: 14 Nov 2007
Posts: 1878
Location: Germany

PostPosted: Tue Dec 04, 2007 3:26 pm    Post subject: Reply with quote

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

If you'd like to get their rank 'adjusted' let me know =)
Btw: it is at least in the Advanced Section Wink

Regards,
plusminus

_________________

| Android Development Community / Tutorials
Back to top
View user's profile Send private message Send e-mail Visit poster's website
evoc
Freshman


Joined: 30 Nov 2007
Posts: 4