42

Solutions

Web Apps. Mobile Apps. Awesome!

Blog

The biggest challenge when putting together the Fencing ScoreKeeper was the TimerView, used to keep track of the remaining time in the bout.

Android Timer Tutorial

April 1, 2010
by

Android featured several timer mechanisms but none of them quite fit the bill.
What was required was:

Simple enough on it’s face, right?

Fortunately or unfortunately, nothing built into the standard Android toolkit did exactly these things. The upside is that Android has the flexibility to write custom views. And that’s just what we did.

This article will explain how we wrote the custom timer and how to run it. We will touch on topics such as extending the View class, resource loading, and threading. If new to Java programming, it is recommended that a primer be read on the basics. However, anyone familiar with Java is sure to catch on to what this article discusses in no time.

Custom Digits

Because the numbers being shown in the view are graphic representations of integers and not the actual integers themselves, a method of processing a variable containing seconds must be written so as to parse and return which digits should be shown. In Fencing ScoreKeeper, a static method was written in a utility class to handle this parsing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    public static final TimeDigits getTimeDigits(final int seconds) {       
        final int SECOND_IN_MINUTE = 60;
        if (seconds == 0) {
            return new TimeDigits(0,0,0,0);
        }
        final int minutes = seconds / SECOND_IN_MINUTE;
        final int secInMin = minutes * SECOND_IN_MINUTE;
        final int leftOverSeconds = seconds - secInMin;
        
        int fM = 0;
        int sM = 0;
        int fS = 0;
        int sS = 0;
        if (minutes >= 10) {
            String minString = (new Integer(minutes).toString());
            fM = new Integer(Character.toString(minString.charAt(0))).intValue();
            sM = new Integer(Character.toString(minString.charAt(1))).intValue();
        } else {
            sM = minutes;
        }
        
        if (leftOverSeconds >= 10) {
            String secString = (new Integer(leftOverSeconds)).toString();
            fS = new Integer(Character.toString(secString.charAt(0))).intValue();
            sS = new Integer(Character.toString(secString.charAt(1))).intValue();
        } else {
            sS = leftOverSeconds;
        }
        return new TimeDigits(fM,sM,fS,sS);
    }
	public static final TimeDigits getTimeDigits(final int seconds) {		
		final int SECOND_IN_MINUTE = 60;
		if (seconds == 0) {
			return new TimeDigits(0,0,0,0);
		}
		final int minutes = seconds / SECOND_IN_MINUTE;
		final int secInMin = minutes * SECOND_IN_MINUTE;
		final int leftOverSeconds = seconds - secInMin;
		
		int fM = 0;
		int sM = 0;
		int fS = 0;
		int sS = 0;
		if (minutes >= 10) {
			String minString = (new Integer(minutes).toString());
			fM = new Integer(Character.toString(minString.charAt(0))).intValue();
			sM = new Integer(Character.toString(minString.charAt(1))).intValue();
		} else {
			sM = minutes;
		}
		
		if (leftOverSeconds >= 10) {
			String secString = (new Integer(leftOverSeconds)).toString();
			fS = new Integer(Character.toString(secString.charAt(0))).intValue();
			sS = new Integer(Character.toString(secString.charAt(1))).intValue();
		} else {
			sS = leftOverSeconds;
		}
		return new TimeDigits(fM,sM,fS,sS);
	}

A holder class TimeDigits was created to hold the digits of the time. As shown here, it holds only two minute digits and two second digits. It could obviously be extended to hold hours as well but for the purposes of Fencing ScoreKeeper where no time longer than ten minutes was to be dealt with, creating this additional flexibility was not required. TimeDigits is what the method returns to the caller and can then be used to show the appropriate digit in the View.

The actual code should be self-explanatory. First it checks to make sure that the seconds passed is not zero. If it is, it returns a TimeDigits instance set to zero. This is to prevent any unnecessary object creation and division by zero later.

Second it divides the number of seconds by the number of minutes into an integer. Then, finding out the number of seconds that represents, it finds out the number of seconds are left over. This is so that if the seconds passed to the method is 232, it discovers there are 3 whole minutes and 52 seconds (262 – (3 * 60) = 52).

From there is parses the digits into individual digits and assigned to the TimerDigits object. Again, to prevent unnecessary processing, the seconds and minutes are checked against 10 to discover whether there is only one or two digits. If not, there is no reason to parse them as a substring.

The TimerView

The first thing to do is create a class that extends the Android View class. When extending the class, override the constructors from the super class as well the onDraw method. As with painting on a canvas in Swing, the onDraw method is called by the graphics subsystem when the view is invalidated and the bulk of rendering the view is done here.

To prevent constant loading of resources from the file system, the images should be loaded by the constructor. Because it’s unknown which constructor is called when, and to prevent duplication of code, it’s best to create a method that is called from the constructor, in this case loadBitmaps.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    private void loadBitmaps(Context context) {
        this.numbers[0] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_0);
        this.numbers[1] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_1);
        this.numbers[2] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_2);
        this.numbers[3] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_3);
        this.numbers[4] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_4);
        this.numbers[5] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_5);
        this.numbers[6] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_6);
        this.numbers[7] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_7);
        this.numbers[8] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_8);
        this.numbers[9] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_9);
        
        this.greenColon = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_colon);
        this.redColon = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_colon_red);   
    }
	private void loadBitmaps(Context context) {
		this.numbers[0] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_0);
		this.numbers[1] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_1);
		this.numbers[2] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_2);
		this.numbers[3] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_3);
		this.numbers[4] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_4);
		this.numbers[5] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_5);
		this.numbers[6] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_6);
		this.numbers[7] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_7);
		this.numbers[8] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_8);
		this.numbers[9] = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_9);
		
		this.greenColon = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_colon);
		this.redColon = BitmapFactory.decodeResource(this.getResources(), R.drawable.time_colon_red);	
	}

Simply put, this method loads and array of bitmaps in numerical order–the bitmap for “0” is at array index 0 and so on. It also loads a bitmap for the colon in the time, one red, the other green though not indicated in the name.

The purpose for the two colons is that red one is to be shown when the timer has paused and the green for when the time is running. This is a nice visual indicator for the user.

So that the view can render the time and know whether it is paused, it requires two helper methods. One sets the time and the other alerts the view as to whether it has been paused.

1
2
3
4
5
6
7
8
public void updateSecondsPassed(int secondsPassed) {
    this.secondsPassed = secondsPassed;
}
 
public void setPaused(boolean paused) {
    this.paused = paused;
    this.invalidate();
}
public void updateSecondsPassed(int secondsPassed) {
	this.secondsPassed = secondsPassed;
}

public void setPaused(boolean paused) {
	this.paused = paused;
	this.invalidate();
}

In the second method, a call to invalidate() is made. This tells the graphics system that the view needs to be redrawn. When running the timer is discussed later, this will be explained in more detail.

The bulk of the work is done in the onDraw() method. This is the method that is given the canvas on which to draw and renders the bitmaps in the correct places.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    protected void onDraw(Canvas canvas) {
        try {       
            TimeDigits digits = Util.getTimeDigits(this.secondsPassed);
                        
            final Bitmap fM = this.numbers[digits.getFirstMinute()];
            final Bitmap sM = this.numbers[digits.getSecondMinute()];
            final Bitmap fS = this.numbers[digits.getFirstSecond()];
            final Bitmap sS = this.numbers[digits.getSecondSecond()];
            
            Bitmap colon = null;
            if (this.paused) {
                colon = this.redColon;
            } else {
                colon = this.greenColon;
            }
                    
            
            canvas.drawBitmap(fM, 0, 0, null);
            canvas.drawBitmap(sM, 100, 0, null);
            canvas.drawBitmap(this.colon, 200, 0, null);
            canvas.drawBitmap(fS, 224, 0, null);
            canvas.drawBitmap(sS, 324, 0, null);
        
        } catch (NullPointerException e) {
            
        }
    }
	protected void onDraw(Canvas canvas) {
		try {		
			TimeDigits digits = Util.getTimeDigits(this.secondsPassed);
						
			final Bitmap fM = this.numbers[digits.getFirstMinute()];
			final Bitmap sM = this.numbers[digits.getSecondMinute()];
			final Bitmap fS = this.numbers[digits.getFirstSecond()];
			final Bitmap sS = this.numbers[digits.getSecondSecond()];
			
			Bitmap colon = null;
			if (this.paused) {
				colon = this.redColon;
			} else {
				colon = this.greenColon;
			}
					
			
			canvas.drawBitmap(fM, 0, 0, null);
			canvas.drawBitmap(sM, 100, 0, null);
			canvas.drawBitmap(this.colon, 200, 0, null);
			canvas.drawBitmap(fS, 224, 0, null);
			canvas.drawBitmap(sS, 324, 0, null);
		
		} catch (NullPointerException e) {
			
		}
	}

This is a simplified version of the actual method used within the ScoreKeeper application. First, it calls getTimeDigits() with the number of seconds passed to the View. Then, accessing the array of digits, it fetches the proper resources into local variables. This isn’t strictly necessary since they could be called in the canvas.drawBitmap() method but it gives the code some clarity.

Then, it checks to see whether it is paused or not and assigns the proper color colon. Once the local variables have been set, it then tells the canvas where to draw the bitmaps.

An interesting development aside, the reason that the entire method is wrapped in a try-catch block looking for NullPointerException is that the XML tools Google provides for developing layouts doesn’t load the bitmap from the file system. Thus, when the layout is checked in visual mode it can’t be seen because of the Exception. Catching the exception allows the layout to be seen, with a blank space representing where the TimerView would otherwise be seen.

The TimerView does load the bitmaps in the emulator however and will render correctly.

Also, in the actual ScoreKeeper application, the coordinates are not hard coded. This is to account for the different screen resolutions available to Android devices. But that’s a whole other article.

But with the four methods, the TimerView is complete. Notice that there is nothing to handle user inputs such as touching. This is because that is handled by the calling Activity to which this view is associated. Also, nothing here actually runs the timer. Again, that is handled by the Activity. The purpose of this View is merely render the digits.

TimerActivity

The actual class in the Fencing ScoreKeeper is called Scoring because it does much more than handle the timer but to keep this article relatively short and sweet, we’re simplifying the Activity to show how to run a timer and handle user inputs such as touches to start and stop the timer.

The first thing to understand about Android is that only the main thread can update views. This is a problem for running a timer that has to start and stop because pausing the main thread causes the application to hang. But since a child thread can’t manipulate a View this seems to leave the developer in a catch-22. Fortunately, Android provides a way out through the Handler service. With a Handler, a child thread can notify the parent that a UI update is required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    Handler timerUpdater = new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case ID_SECOND_PASSED:
                    if (running) {
                        secondsPassed--;
                    }
                case ID_TIMER_UPDATE:                                   
                timer.updateSecondsPassed(secondsPassed);
                    break;
            }
            timer.setPaused(!running);
            timer.invalidate();
        }
    };
	Handler timerUpdater = new Handler() {
		public void handleMessage(Message msg) {
			switch (msg.what) {
				case ID_SECOND_PASSED:
					if (running) {
						secondsPassed--;
					}
				case ID_TIMER_UPDATE:									
				timer.updateSecondsPassed(secondsPassed);
					break;
			}
			timer.setPaused(!running);
			timer.invalidate();
		}
	};

The Handler in ScoreKeeper at its base is what’s shown above. It receives a message from the child thread and based upon a constant defined that is assigned to the message takes the action requested. In this case, two messages can be received, one telling it that a second has passed–to be checked against whether the timer is running–and the second to update the TimerView with the number of seconds passed. Since, if a second has passed, the TimerView will need to be updated there is no break statement in the first case so that it will flow into the second.

Here, the number of seconds decrements though it can just as easily be made to increment.

Lastly, the invalidate method is called. By invalidating the view, the graphics subsystem is informed that the view needs to be re-rendered. When it is re-rendered, the TimerView‘s onDraw() method is called and the new time is displayed in the View. And because the Handler is in the main thread, this is a valid call to update the UI.

The actual thread itself is very simple.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    class timerRunner implements Runnable {
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                if (running) {
                    Message msg = new Message();
                    msg.what = ID_SECOND_PASSED;
                    SimpleTimerActivity.this.timerUpdater.sendMessage(msg);
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
	class timerRunner implements Runnable {
		public void run() {
			while (!Thread.currentThread().isInterrupted()) {
				if (running) {
					Message msg = new Message();
					msg.what = ID_SECOND_PASSED;
					SimpleTimerActivity.this.timerUpdater.sendMessage(msg);
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				}
			}
		}
	}

All this does is check to see if it’s running and, if so, send a message to the Activity‘s Handler. Then it sleeps for a second. Simple indeed.

To start and stop the thread, two helper methods were created. In these methods, instances of timerRunner are either created or destroyed based upon its state. This is to handle situations where the activity might have to be paused and where it won’t surrender itself with a child thread running.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    protected void stopTimer() {
        if (this.timer != null) {
            this.timer.setPaused(true);
            this.timer.invalidate();
        }
        if (this.timerThread != null) {
            this.timerThread.interrupt();
            this.timerThread = null;
        }
    }
 
    protected void startTimer(Runnable runable) {
        if (this.timerThread == null) {
            this.timerThread = new Thread(runable);
        }
        if (!this.timerThread.isAlive()) {
            this.timerThread.start();
        }
    }
	protected void stopTimer() {
		if (this.timer != null) {
			this.timer.setPaused(true);
			this.timer.invalidate();
		}
		if (this.timerThread != null) {
			this.timerThread.interrupt();
			this.timerThread = null;
		}
	}

	protected void startTimer(Runnable runable) {
		if (this.timerThread == null) {
			this.timerThread = new Thread(runable);
		}
		if (!this.timerThread.isAlive()) {
			this.timerThread.start();
		}
	}

Lastly, to start and stop the timer, a listener must be created to listen for touch events. In this case, the act is binary. If the timer is running, stop it. If it’s paused, start it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    private OnTouchListener timerListener = new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                Message msg = new Message();
                msg.what = ID_TIMER_UPDATE;
                
                running = !running;
                startTimer(new timerRunner());
                
                timer.setPaused(!running);
                timerUpdater.sendMessage(msg);
            }
            return true;
        }
    };
	private OnTouchListener timerListener = new OnTouchListener() {
		@Override
		public boolean onTouch(View v, MotionEvent event) {
			if (event.getAction() == MotionEvent.ACTION_UP) {
				Message msg = new Message();
				msg.what = ID_TIMER_UPDATE;
				
				running = !running;
				startTimer(new timerRunner());
				
				timer.setPaused(!running);
				timerUpdater.sendMessage(msg);
			}
			return true;
		}
	};

Like the thread, it creates a message for the handler telling it to update the TimerView. If it’s running, it pauses, if it’s paused it runs. The startTimer() call is to ensure that a thread is available and active. If one already exists, it will be ignored but if the thread is null, it will be created. However, if the timer is paused, the thread will run but no updates to the TimerView will take places.

And that’s it!

Conclusion

With one custom View, a timer can be created. In Fencing ScoreKeeper it’s used to display a timer with a nice digital font. However, this can be used to create any kind of timer. But with the correct images it can be used to make something like Lost’s Hatch Countdown Timer or even a custom clock.

As Einstein taught us in Farscape, time is pretty important. It might as well be pretty, too.

Need a custom mobile application? 42 Solutions can help. Be it the latest Android device or an iPhone, 42 Solutions can meet your mobile needs, bringing the power of your website into the palm of your customers’ hands. Contact 42 Solutions today.

Back to Blog

42 Tags: , , , , , ,

Comments

Content for class "bottomYellow" Goes Here
Content for class "bottomBlue" Goes Here

42 Solutions

Working with 42 Solutions, you’ll find that it’s not the technology that’s important, it’s the idea. Regardless of whether your company is based on proprietary software or open source, we will find the right tools to get the job done.

Invite us to your office and we’ll send a pair of consultants armed with laptops and bright ideas, ready to solve your problem and offer solutions. We pride ourselves in getting to know how you do business and integrating into your process.