Smartphones

Capture the flag: A tutorial on Android's Canvas class and Touch event handler

Android developer William Francis takes a break from writing business apps and shows how to code a simple electronic game of capture the flag.

Step seven: Besides a method to hide the flag, we will also want to provide a way for the user to give up. In this case, we just toggle the global isHiddenFlag, and again force the screen to re-draw calling the invalidate() method.
public void giveUp(){
isFlagHidden = false;

invalidate();

}
Step eight: Now we come to the onDraw method. On the Android platform, overriding the onDraw is a common technique used to manually paint the contents of a view. You must draw everything within this function each time the method is called. For us that means maybe the image of our flag (depending if it is "hidden" or not) and a simple gray rectangle that acts as a visual indicator to the user where the game board is. There is also an initialization clause, where we check to see if our flag x and y location have been set, and if not, we initialize both the size of our canvas and prepare to draw the flag in the center of it.
@Override
public void onDraw(Canvas canvas) {
if ((mFlagX < 1) || (mFlagY < 1)) {
mFlagX = (int) (getWidth() / 2) - mBitmap.getWidth() / 2;
mFlagY = (int) (getHeight() / 2) - mBitmap.getHeight() / 2;
mBoundX = (int)getWidth() - mBitmap.getWidth();
mBoundY = (int)getHeight() - mBitmap.getHeight();
}
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
if (!isFlagHidden) {
canvas.drawBitmap(mBitmap, mFlagX, mFlagY, null);

}

}

Gotcha! A common beginner's mistake is to want to do the initialization of the canvas width and height in the constructor. However, the width and height of a View is not actually set until the first pass of the layout happens. getWidth() and getHeight() will always return 0 in the constructor. Since we need the width and height to determine the initial x and y parameters of the mFlag as well, a conditional initialization in the onDraw works nicely.

Step nine: The trickiest part of this tutorial is the logic in our collision detection algorithm. This is where we use the constants CLOSE and CLOSER, which we defined in step five. The diagram in Figure B should help explain how these variables are used, as well as how tweaking them can increase of decrease the sensitivity of the game. Figure B

Based on an algorithm of concentric rectangles, we use Android's built-in Rect data class to create the outer two rectangles. We must include a bit of normalization code to keep any rectangle from extending off the bounds of our canvas. After that, it's just a matter of using the Rect class's .contains() method. We must start in the center and work our way outward.

public Indicators takeAGuess(float x, float y) {
//this is our "warm" area
Rect prettyClose = new Rect(mFlagX - CLOSE, mFlagY - CLOSE, mFlagX+mBitmap.getWidth() + CLOSE, mFlagY+mBitmap.getHeight() + CLOSE);

//normalize

if (prettyClose.left < 0) prettyClose.left = 0; if (prettyClose.top < 0) prettyClose.top = 0; if (prettyClose.right > mBoundX) prettyClose.right = mBoundX; if (prettyClose.bottom > mBoundY) prettyClose.bottom = mBoundY;
//this is our "hot" area
Rect reallyClose = new Rect(mFlagX - CLOSER, mFlagY - CLOSER, mFlagX+mBitmap.getWidth() + CLOSER, mFlagY+mBitmap.getHeight() + CLOSER);
//normalize
if (reallyClose.left < 0) reallyClose.left = 0;
if (reallyClose.top < 0) reallyClose.top = 0;
if (reallyClose.right > mBoundX) reallyClose.right = mBoundX;
if (reallyClose.bottom > mBoundY) reallyClose.bottom = mBoundY;
//this is the area that contains our flag
Rect bullsEye = new Rect(mFlagX, mFlagY, mFlagX+mBitmap.getWidth(), mFlagY+mBitmap.getHeight());
//check to see where on the board the user pressed
if (bullsEye.contains((int) x, (int)y)) {
//found it
isFlagHidden = false;
invalidate();
return Indicators.BULLSEYE;
} else if (reallyClose.contains((int) x, (int)y)) {
//hot
return Indicators.HOT;
} else if (prettyClose.contains((int)x, (int)y)) {

//warm

return Indicators.WARM; } else {

//not even close

return Indicators.COLD;

}

}
Step 10: With our GameBoard class doing the heavy lifting, all that remains is to implement our UI and "glue" code in Main.java. We start with a standard class that extends Activity, implementing both an on touch and on click listener. There are also a couple of class variables: one for toggling whether our flag is hidden and one for holding the drawing canvas.
package com.authorwjf;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
import android.widget.TextView;
public class Main extends Activity implements OnTouchListener, OnClickListener{
private GameBoard mGameBoard = null;
private boolean isFlagHidden = false;
/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

setContentView(R.layout.main);

mGameBoard = (GameBoard) findViewById(R.id.the_canvas);

mGameBoard.setOnTouchListener(this);

Button b = (Button) findViewById(R.id.the_button);

b.setOnClickListener(this);

}

}
Step 11: After wiring the listeners in our onCreate override, we need to implement the handlers. Let's focus on the onClick first. This method gets called in response to the button press. We invert the isFlagHidden state using a logical not operator (!). Then we call hideTheFlag() or giveUp() in our GameBoard class as appropriate.
@Override
public void onClick(View v) {
if (v.getId() == R.id.the_button) {
TextView tv = (TextView)findViewById (R.id.the_label);
tv.setText("");
Button b = (Button) findViewById(R.id.the_button);
isFlagHidden = !isFlagHidden;
if (isFlagHidden) {
b.setText("Give Up!");
mGameBoard.hideTheFlag();
} else {
b.setText("Hide the Flag!");
mGameBoard.giveUp();
}
}
}
Step 12: We need to intercept and respond to the Touch events. We've already implemented all the methods we need to determine whether a user had found the flag. We just need to call our takeAGuess() method inside the GameBoard class, passing as parameters the current x and y coordinates of the user's finger. We also need to update the text label to let the user know what's up.
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getId() == R.id.the_canvas) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (isFlagHidden) {
TextView tv = (TextView)findViewById (R.id.the_label);
switch (mGameBoard.takeAGuess(event.getX(), event.getY())) {
case BULLSEYE:
Button b = (Button) findViewById(R.id.the_button);
isFlagHidden = false;
b.setText("Hide the Flag!");
tv.setText("You found it!");
tv.setTextColor(Color.GREEN);
break;
case HOT:
tv.setText("You're hot!");
tv.setTextColor(Color.RED);
break;
case WARM:

tv.setText("Getting warm...");

tv.setTextColor(Color.YELLOW);

break; case COLD:

tv.setText("You're cold.");

tv.setTextColor(Color.BLUE);

break;

}

}

}

return true;

}

return false;
}

If you managed to follow the tutorial up to this point, you should have your own playable version of capture the flag. For those of you who'd prefer to download and import the entire project into Eclipse, you can grab it here.

Thank you to those readers who contacted me concerning the soundboard tutorial and requested I do something similar with Android's graphic subsystem. After spending all week at my daytime gig writing serious business apps, coding and sharing this simple game was a fun change of scenery. I encourage anyone reading this post to use the discussion thread or the email contact form if you have ideas for other subjects you'd like me to cover in the App Builder blog.

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

1 comments
Paulo Cezar
Paulo Cezar

Great article, presenting a complete app step by step. Thanks for sharing.

Editor's Picks