Somebody once said, “If it’s not broken, fix it anyway.” Not the traditional phrase for sure, but the old version relies on one assumption that can’t ever hold true. I don’t know if I pursued this project in the best possible way. The only way I’ll find out is if I try it differently.
You should probably start a new project. We’re going to be rebuilding everything from the ground up.
When we finished the last version, we had a single custom View subclass which did all kinds of things to move nicely scaled and rendered bitmaps around inside itself. It worked. We learned a lot. It was fiddly.
The new plan is to start with a ViewGroup. We’ll load it up with ImageViews for pieces, and more ImageViews to highlight the valid moves that will be shown when a piece is selected. We’ll tag the pieces with their type and tag the highlights with “MOVE” so we can tell them apart later on.
ViewGroups are specially built to organise and layout child views. We’d almost get away with calling it a BoardLayout. Let’s crack it open and take a look:
[fragment name=”ViewGroupBoard.java” root]
public class ViewGroupBoard extends ViewGroup {
[fragment name=”class variables”]
[fragment name=”constructor”]
[fragment name=”onMeasure”]
[fragment name=”onLayout”]
[fragment name=”onDraw”]
[fragment name=”set or get MovingPiece”]
[fragment name=”getViewAtLocation”]
[fragment name=”clear move highlights”]
[fragment name=”handle history”]
}[/fragment]
Phew! That looks like it’s going to become a lot of code, but we can steal liberally from the old version so we should be able to rock through it pretty quickly. Let’s start out with the basic class variables and initialise them. We’ll need to know the height and width of the tiles, both for the tiles and for measuring the pieces and highlights. We’ll also need a paint object to help us draw the board.
[fragment name=”class variables”]
private int mTileWidth = 0;
private int mTileHeight = 0;
private Paint mDark = new Paint();
[/fragment]
[fragment name=”constructor”]
public ViewGroupBoard(Context context, AttributeSet attrs) {
//Constructor
super(context, attrs);
//configure the paint object
mDark.setColor(Color.parseColor(“#AAAA88”));
//draw the board backgound
setWillNotDraw(false);
[fragment name=”click handler”]
}[/fragment]
Looks easy enough, but that setWillNotDraw() is new. The fact is layouts tend not to have backgrounds, you infer their presence by what they do to their child views, so by default ViewGroups do not call their own onDraw() event. This overrides that so we can get our checker board back.
We’ll explain the click handler later.
Next we want to measure everything that needs it. Just like before, the activity will have a good idea of the space the board can take up. We’ll take whatever we are given and work out the largest square that will fit in that space to draw our board.
[fragment name=”onMeasure”]
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (width < height) { height = width; }
mTileWidth = width / 8;
mTileHeight = height / 8;
setMeasuredDimension(width, height);
}[/fragment]
If we were uncertain about how big the child view could or should be, we'd call measureChildren(). Possibly a few times with different settings to obtain an optimal layout. However we know already that the pieces should be one tile square in size so we can skip that part.
Now we know how big everything is going to be, we can lay it all out. The child views are held in the order they were added, so we expect to have just been given 24 pieces of one colour, followed by 24 pieces of the other. In the middle we'll bump the 'top' value to leave two rows empty. We only want to do this at the start of a new game though or we'd keep re-organising the pieces every time a move was made, so let's get us a new game boolean.
[fragment name="class variables"]
private boolean mNewGame = true;
public void newGame() {
mNewGame=true;
}
[/fragment]
[fragment name="onLayout"]
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mNewGame) {
for (int i = 0; i < getChildCount(); i++) {
int top = i / 8;
int left = i % 8;
if (i >=24) {
top += 2;
}
View child = getChildAt(i);
child.layout(left*mTileWidth, top*mTileHeight, (left+1)*mTileWidth, (top+1)*mTileHeight);
}
mNewGame = false;
}
}[/fragment]
We could make this more generic, but let’s keep things simple for clarity. child.layout() is the magic that sets the piece on the board. Everything is laid out relative to the ViewGroup rather than the screen, so 0,0 is the top left corner of the board. We multiply everything by with width of the pieces we worked out while measuring them.
Now we can look at drawing the board proper. We can override the onDraw function since we put it back into service in the constructor.
[fragment name=”onDraw”]
@Override
public void onDraw(Canvas canvas) {
drawBoard(canvas);
}
private void drawBoard(Canvas canvas) {
//Fill the board with a single colour
canvas.drawColor(Color.parseColor(“#FFFFAA”));
//now fill alternate squares
for (int i = 0; i < 8 ; i++) {
for (int j = 0; j < 8 ; j++) {
if ((i+j) % 2 == 0)
canvas.drawRect(j * mTileWidth , i * mTileHeight , (j+1) * mTileWidth , (i+1) * mTileHeight, mDark);
}
}
}[/fragment]
Just like before, we are filling the whole board with a single light colour and filling the alternate squares with the dark paint object we created earlier.
One other thing we want the board to keep track of is the piece that's currently selected for movement. This is very simple:
[fragment name="class variables"]
private View mMovingPiece = null;
[/fragment]
[fragment name="get or set movingPiece"]
public void setMovingPiece(View v) {
mMovingPiece = v;
}
public View getMovingPiece() {
return mMovingPiece;
}[/fragment]
One of the new things we are going to need to do is find if a child is at a particular location on the board. There doesn't seem to be an easy way to do it, so we'll have to do it by hand. We want a function that will take co-ordinates and return the child view at that location if one exists. When we start splashing move highlights over the board we're going to want to ignore them here. Happily we already know how to spot them.
[fragment name="getViewAtLocation"]
public View getViewAtLocation(int left, int top) {
//loop through the child collection
for (int i = 0; i < getChildCount(); i++) {
//get the child's hit rectangle
Rect r = new Rect();
View child = getChildAt(i);
child.getHitRect(r);
//Don't return "MOVE" children. Then check if the co-ordinates are inside the hit rectangle
if (child.getTag() != "MOVE" && r.contains(left, top))
return child;
}
//nothing found
return null;
}[/fragment]
While we are on the topic of move highlights, we're going to want a helper function that will clear them all away quickly. This time though, we can find them with that tag.
[fragment name="clear move highlights"]
public void clearMoves() {
View v = null;
while ((v = findViewWithTag("MOVE")) != null) {
removeView(v);
}
}[/fragment]
We put a placeholder in for the board's onClickListener in the constructor as well. All we want to happen when the user clicks on the board is for all the displayed moves to disappear, and now we have something that'll do that for us.
[fragment name="click handler"]
//clear any present moves if they are set on the board
setOnClickListener(new OnClickListener(){
public void onClick(View v) {
((ViewGroupBoard)v).clearMoves();
}
});[/fragment]
The last thing we want the board to do it track the move history and be able to undo it. Once again we are stealing lots of code from the previous version of the board, starting with the Stack object that will store the history.
[fragment name="class variables"]
private class move {
View movingPiece;
int mTop;
int mLeft;
View capturedPiece;
int cTop;
int cLeft;
}
final Stack<move> history = new Stack<move>();[/fragment]
Information about moves being made will be pushed in from the outside, so we'll need a method to accept it. Once we have the information, we can provide an Undo method that will pop a move off the stack and reverse it. The undo function will be called from the activity trapping the play pressing the back key on the device.
[fragment name="handle history"]
public void addHistory(View movingPiece, int mTop, int mLeft, View capturedPiece, int cTop, int cLeft) {
final move m = new move();
m.movingPiece = movingPiece;
m.mTop = mTop;
m.mLeft = mLeft;
m.capturedPiece = capturedPiece;
m.cTop = cTop;
m.cLeft = cLeft;
history.push(m);
}
public void undo() {
clearMoves();
//If there is no history, then don't do anything
if (history.size() == 0) return;
//Get the most recent move
final move m = history.pop();
//put the moving piece back.
m.movingPiece.layout(m.mLeft, m.mTop, m.mLeft + mTileWidth, m.mTop + mTileHeight);
//If a piece was captured in this move then put it back too
if (m.capturedPiece != null)
handleCapturedPiece(m);
}
private void handleCapturedPiece(Move m) {
//Lay it out on the board and add it
m.capturedPiece.layout(m.cLeft, m.cTop, m.cLeft + mTileWidth, m.cTop + mTileHeight);
m.capturedPiece.setClickable(true);
addView(m.capturedPiece);
}[/fragment]
And that's us done. If you've followed all that you might be asking when the child views get drawn. There is a drawChild method we can override, but we're not doing anything interesting with it, so we don't need to. Android will call it when it's good and ready and everything will slot into place. You may also be wondering why handling of the captured piece is in a function by itself. I'll let you in on that one later.
Replace the old board with this new one, and start loading some ImageViews into it with addView(). It ought to look like the old game in no time. In the next post we’ll look at creating an OnClickListener for those images view objects that will be the first half of the movement code. For the new board I’ve given the pieces a makeover. Yes; I still cannot draw for toffee.