Following on from part 1, where we showed screens which updated only on finger press events, its time to get dynamic. We'll build on the first tutorial and make the numbers spin up all the time. This is a little more complex because it introduces a fundamental rule regarding threading on Android - one which was smartly side-stepped in part 1.
A finger tap will still bring up new counters - the difference is we will now see them spinning up in response to the background thread.
There was a reason the counters didn't continuously update in the first threading tutorial - it was to avoid the thorny issue seasoned UI developers are only too well aware of: you cannot update the UI directly from a thread. This is a golden rule on Android. There is nothing in the code stopping you trying - no syntax errors will be thrown, and your code will attempt to run. Thats because these are normal java objects and obey all the regular method calling rules. When you do try it though, your app will crash spectacularly.
The reason for this is the UI is updated from the main thread of all Android apps which expects to have exclusive access to it. That means the internal state mechanisms are only updated by known system code in a controlled manner. When a separate thread attempts to access this, which by its nature is asynchronous so will be interrupting the main thread at any time, it would need to disturb these internal state mechanisms. If this were to happen, it would not have informed the main thread of what it had done. So the main thread finds the nice, ordered internal data structures it has been maintaining are being changed without its knowledge - it will quickly cause havoc to the whole application.
This sounds bad - it seems we can have all the wonders of simultaneous parallel code execution but we can't see what anything these separate threads do? Not quite. The rule is the extra threads cannot make UI calls directly - they can make as many calls into the main thread as they want as long as they don't touch the UI. A great way of making these calls back is to send it a Message. This is a controlled bundle of data which is created by the Message sender and aimed at any object with a Message receiver. When it is fired the receiver can extract this data and make use of it. Theres a MessageQueue tutorial available.
So this is a great way forward for our example - we could get the background thread to continue updating the counters but send the main thread a Message each time there is a new value. The main thread would handle this message by updating the counters, and in effect we will see them all updating continuously.
Take a look at this in action:
Message Handling
The threading2 class implements runnable. This means it must provide a run() method, and its this which is called to run in parallel with the main program. As in Threading1, we use it to update the ticker value, but this time we also send a Message back to the main thread each time the ticker changes:
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(50);
} catch (Exception e) {}
// Update the ticker
mTicker++;
// Tell the main app it needs updating
if (mCounterPositions.size() > 0) {
Message msg = Message.obtain();
msg.arg1 = MSG_UPDATE;
handler.sendMessageDelayed(msg, MSG_UPDATE);
}
}
}
The code to handle the incoming message is provided by the Handler as follows:
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.arg1) {
case MSG_UPDATE: {
invalidate();
break;
}
}
}
};
Notice the handler literally does nothing apart from call invalidate(), which in turn causes the main Views onDraw() method to be called. Of course the values the thread maintains will have been updated at that time, so when the draw does occur it will show the changed value.
If the counter maintaining thread had attempted to call the main threads invalidate() or onDraw() method, either directly or indirectly, the crash described earlier would occur.
This is a small but imporant extension to Threading1. In the next part, Threading3, we'll expand on this idea by adding another thread and show how to organise the source better in light of the complexity we are starting to see with these apps.
Source
Heres the source so you can browse it online, the supporting Android XML files and resources are all available from the download link in the download section.
/*
* Copyright (c) 2009 OTAMate Technology Ltd. All Rights Reserved.
* http://www.androidacademy.com
*/
package com.androidacademy.tutorials.threading2;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
/**
* App to demonstrate a simple threading process.
* Numbers are drawn on the screen inside circles in response to
* a finger press event. The numbers continuously change in response to
* an update message from a background thread. In other respects this
* is the same app as Threading1.
*/
public class Threading2Activity extends Activity {
public static final int MENU_ABOUT = Menu.FIRST;
private Threading2View mThreading2View;
/**
* Handle the onCreate function
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mThreading2View = new Threading2View(this);
setContentView(mThreading2View);
// Start the background thread to increment the ticker
Thread thread = new Thread(mThreading2View);
thread.start();
}
/**
* The main view responsible for drawing the display, handling
* the finger press events and creating the background thread.
*/
private static class Threading2View extends View
implements Runnable {
private final Paint mPaint = new Paint();
private Bitmap mBmBackground;
private List<Point> mCounterPositions = new ArrayList<Point>();
private int mTicker = 0;
private static final int MSG_UPDATE = 1;
public Threading2View(Context context) {
super(context);
setFocusable(true);
mBmBackground = BitmapFactory.decodeResource(getResources(),
R.drawable.backgound);
mPaint.setTextSize(16);
mPaint.setTextAlign(Paint.Align.CENTER);
}
/**
* Draw the display
*/
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBmBackground, 0, 0, mPaint);
// Don't try to draw anything if there are no items yet
if (mCounterPositions.size() > 0) {
int counter = 0;
for (Point position: mCounterPositions) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPaint.setAntiAlias(true);
mPaint.setColor(Color.YELLOW);
canvas.drawCircle(position.x, position.y, 25, mPaint);
// Draw the value inside the circle
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(0);
String count = ""+((counter++) + mTicker);
canvas.drawText(count, position.x, position.y + 6, mPaint);
}
}
}
/**
* Handle the finger press event by creating a new
* number and adding it to in the list of counters
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Point position = new Point
((int) event.getX(), (int) event.getY() );
// Add the position
mCounterPositions.add(position);
invalidate();
break;
}
return true;
}
/**
* Handle the update message from the thread.
* Invoke the main display update each time
* one is received.
*/
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.arg1) {
case MSG_UPDATE: {
invalidate();
break;
}
}
}
};
/**
* Create the background thread to update the main
* ticker value at the same time as the main programs
* thread is running.
*/
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(50);
} catch (Exception e) {}
// Update the ticker
mTicker++;
// Tell the main app it needs updating
if (mCounterPositions.size() > 0) {
Message msg = Message.obtain();
msg.arg1 = MSG_UPDATE;
handler.sendMessageDelayed(msg, MSG_UPDATE);
}
}
}
}
}
Portions are modifications based on work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License. Android Academy is independent from Google. All trademarks acknowledged.