1. Creating a Custom View for the Tiles
In a previous tutorial I described how you could create a custom view in Android that will take attributes and implement custom logic. This is the approach we will take for creating the full Tic Tac Toe application.
This custom view will be called the TicTacToeTileView. This class will extend an image view to take advantage of the ability of an ImageView to easily draw a graphic.
The constructor for the tile will take four boolean attributes, one for each border of the tile. If the boolean is set than that side of the tile, a black border will be drawn.
Listed below is the code for the constructor.
public TicTacToeTile(Context context, AttributeSet attrs) { super(context, attrs); // Get the border information TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.com_dreamdom_tictactoe_TicTacToeTile); mBorderLeft = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderLeft, false); mBorderRight = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderRight, false); mBorderTop = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderTop, false); mBorderBottom = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderBottom, false); // Get the row and column information mRow = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_row, 0); mCol = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_col, 0); initTile(); }
Since this class implements TicTacToeDrawable, the class is required to implement the setState method. This method will set the tile to one of three possible states - X,O, or blank.
In the TicTacToeTileView's onDraw method is where we will draw the borders. This code is highlighted below.
@Override protected void onDraw(Canvas canvas) { // Always call the super super.onDraw(canvas); // Always draw the borders if (mBorderRight == true) canvas.drawLine(mWidth, 0, mWidth, mHeight, mBorderPaint); if (mBorderLeft == true) canvas.drawLine(0, 0, 0, mHeight, mBorderPaint); if (mBorderTop == true) canvas.drawLine(0, 0, mWidth, 0, mBorderPaint); if (mBorderBottom == true) canvas.drawLine(0, mHeight, mWidth, mHeight, mBorderPaint); }
The entire TicTacToeTile.java is listed below
package com.dreamdom.tictactoe; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import com.dreamdom.tictactoe.gamedriver.TicTacToeConstants; import com.dreamdom.tictactoe.gamedriver.TicTacToeDrawable; public class TicTacToeTile extends ImageView implements TicTacToeDrawable { // Constants public static final int DEFAULT_PAINT_WIDTH = 10; // Dimensions private int mWidth; private int mHeight; // Borders private Paint mBorderPaint; private boolean mBorderLeft = false; private boolean mBorderTop = false; private boolean mBorderRight = false; private boolean mBorderBottom = false; // Drawables private static Drawable drawableX; private static Drawable drawableO; private static Drawable drawableBlank; // Game related private int mState; private int mRow; private int mCol; public TicTacToeTile(Context context) { super(context); initTile(); } public TicTacToeTile(Context context, AttributeSet attrs) { super(context, attrs); // Get the border information TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.com_dreamdom_tictactoe_TicTacToeTile); mBorderLeft = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderLeft, false); mBorderRight = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderRight, false); mBorderTop = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderTop, false); mBorderBottom = a.getBoolean( R.styleable.com_dreamdom_tictactoe_TicTacToeTile_borderBottom, false); // Get the row and column information mRow = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_row, 0); mCol = a.getInteger(R.styleable.com_dreamdom_tictactoe_TicTacToeTile_col, 0); initTile(); } private void initTile() { // Create the paint mBorderPaint = new Paint(); mBorderPaint.setStrokeWidth(getContext().getResources() .getDisplayMetrics().density * DEFAULT_PAINT_WIDTH); mBorderPaint.setColor(Color.BLACK); // Default the state to empty mState = TicTacToeConstants.TILE_STATE_EMPTY; } public static void setDrawableX(Drawable x) { drawableX = x; } public static void setDrawableO(Drawable o) { drawableO = o; } public static void setDrawableBlank(Drawable blank) { drawableBlank = blank; } public int getRow() { return mRow; } public int getCol() { return mCol; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Save the dimensions mWidth = getWidth(); mHeight = getHeight(); } @Override public void setState(int state) { mState = state; if(state == TicTacToeConstants.TILE_STATE_X) { setImageDrawable(drawableX); } else if (state == TicTacToeConstants.TILE_STATE_O) { setImageDrawable(drawableO); } else { setImageDrawable(drawableBlank); } // force a redraw invalidate(); } @Override public int getState() { return mState; } @Override protected void onDraw(Canvas canvas) { // Always call the super super.onDraw(canvas); // Always draw the borders if (mBorderRight == true) canvas.drawLine(mWidth, 0, mWidth, mHeight, mBorderPaint); if (mBorderLeft == true) canvas.drawLine(0, 0, 0, mHeight, mBorderPaint); if (mBorderTop == true) canvas.drawLine(0, 0, mWidth, 0, mBorderPaint); if (mBorderBottom == true) canvas.drawLine(0, mHeight, mWidth, mHeight, mBorderPaint); } }
2. Creating a Layout for the Application
Now, we will build a layout for the application. In our layout, we will want to reference the custom view that we created in the first step.
We will also have our layout feature two buttons--one to play the "easy" AI and one to play the "hard" AI. We will also include a checkmark to select whether the user can go first or not, and a label that displays the status of the game.
The full code for the layout main.xml is listed below.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:myapp="http://schemas.android.com/apk/res/com.dreamdom.tictactoe" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@drawable/bluestripe4"> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayoutToolbar" android:layout_gravity="center" android:background="#000000"> <CheckBox android:layout_width="wrap_content" android:text="@string/player_goes_first" android:layout_height="wrap_content" android:id="@+id/checkBoxPlayerFirst" android:textStyle="bold" android:textColor="#ffffff" android:checked="true"></CheckBox> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/buttonEasyGame" android:text="@string/easy" android:minWidth="75dip"></Button> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/buttonHardGame" android:text="@string/hard" android:minWidth="75dip"></Button> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/linearLayoutMessage" android:background="#000000" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="3dip" android:layout_gravity="center" android:id="@+id/textViewStatusMessage" android:text="@string/playing_game" android:textSize="18dip"></TextView> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:id="@+id/linearLayoutGameBoard" android:background="@drawable/whitesquare" android:layout_height="wrap_content" android:padding="10dip" android:layout_margin="20dip"> <TableLayout android:id="@+id/tableLayout1" android:layout_width="fill_parent" android:weightSum="3" android:layout_height="wrap_content" android:layout_marginLeft="10dip" android:layout_marginRight="10dip"> <TableRow android:id="@+id/tableRow1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"> <com.dreamdom.tictactoe.TicTacToeTile android:adjustViewBounds="true" android:scaleType="fitXY" android:id="@+id/GameTile01" android:layout_height="wrap_content" myapp:row="0" myapp:col="0" android:layout_weight="1" android:layout_width="fill_parent" android:src="@drawable/blank"/> <com.dreamdom.tictactoe.TicTacToeTile android:layout_weight="1" android:scaleType="fitXY" android:adjustViewBounds="true" android:id="@+id/GameTile02" android:layout_height="wrap_content" myapp:row="0" myapp:col="1" myapp:borderLeft="true" myapp:borderRight="true" android:layout_width="fill_parent" android:src="@drawable/blank"/> <com.dreamdom.tictactoe.TicTacToeTile android:adjustViewBounds="true" android:scaleType="fitXY" android:layout_weight="1" android:id="@+id/GameTile03" myapp:row="0" myapp:col="2" android:layout_height="wrap_content" android:layout_width="fill_parent" android:src="@drawable/blank"/> </TableRow> <TableRow android:id="@+id/tableRow2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"> <com.dreamdom.tictactoe.TicTacToeTile android:layout_width="wrap_content" android:adjustViewBounds="true" android:scaleType="fitXY" android:id="@+id/GameTile04" myapp:borderTop="true" myapp:borderBottom="true" myapp:row="1" myapp:col="0" android:layout_height="wrap_content" android:layout_weight="1" android:src="@drawable/blank"/> <com.dreamdom.tictactoe.TicTacToeTile android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:scaleType="fitXY" android:adjustViewBounds="true" myapp:borderTop="true" myapp:borderBottom="true" myapp:borderLeft="true" myapp:borderRight="true" myapp:row="1" myapp:col="1" android:id="@+id/GameTile05" android:src="@drawable/blank"/> <com.dreamdom.tictactoe.TicTacToeTile android:layout_width="wrap_content" android:layout_height="wrap_content" android:adjustViewBounds="true" android:scaleType="fitXY" android:layout_weight="1" myapp:borderTop="true" myapp:borderBottom="true" myapp:row="1" myapp:col="2" android:id="@+id/GameTile06" android:src="@drawable/blank"/> </TableRow> <TableRow android:id="@+id/tableRow3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"> <com.dreamdom.tictactoe.TicTacToeTile android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:adjustViewBounds="true" myapp:row="2" myapp:col="0" android:scaleType="fitXY" android:id="@+id/GameTile07" android:src="@drawable/blank"/> <com.dreamdom.tictactoe.TicTacToeTile android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:scaleType="fitXY" android:adjustViewBounds="true" myapp:borderLeft="true" myapp:borderRight="true" myapp:row="2" myapp:col="1" android:id="@+id/GameTile08" android:src="@drawable/blank"/> <com.dreamdom.tictactoe.TicTacToeTile android:layout_width="wrap_content" android:layout_height="wrap_content" android:adjustViewBounds="true" android:scaleType="fitXY" android:layout_weight="1" myapp:row="2" myapp:col="2" android:id="@+id/GameTile09" android:src="@drawable/blank"/> </TableRow> </TableLayout> </LinearLayout> </LinearLayout>
3. Creating the GameActivity Class
Lastly, we must create the GameActivity class. This class will be fairly straightforward. Since most of the logic is contained in the GameDriver package, the GameActivity class will mostly be responsible for listening for button clicks, and passing on the value of these button clicks to the game manager.
The code for the GameActivity class is listed below.
package com.dreamdom.tictactoe; import java.util.ArrayList; import com.dreamdom.tictactoe.gamedriver.ClassicGameManager; import com.dreamdom.tictactoe.gamedriver.EasyGameAI; import com.dreamdom.tictactoe.gamedriver.GameManager; import com.dreamdom.tictactoe.gamedriver.HardGameAI; import com.dreamdom.tictactoe.gamedriver.TicTacToeConstants; import com.dreamdom.tictactoe.gamedriver.TicTacToeDrawable; import android.app.Activity; import android.content.res.Resources; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.TextView; import android.widget.CompoundButton.OnCheckedChangeListener; /** * GameActivity is used to handle 3 by 3 tic tac toe games * @author Dominic * */ public class GameActivity extends Activity { public static final int[] GAME_TILE_IDS = { R.id.GameTile01, R.id.GameTile02, R.id.GameTile03, R.id.GameTile04, R.id.GameTile05, R.id.GameTile06, R.id.GameTile07, R.id.GameTile08, R.id.GameTile09 }; private GameManager mGameManager; private TileClickHandler mClickHandler; private EasyGameAI mEasyGameAI; private HardGameAI mHardGameAI; private TextView mTextViewStatus; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Set the main content view setContentView(R.layout.main); // Set the drawables for the tiles TicTacToeTile.setDrawableX(getResources().getDrawable(R.drawable.x)); TicTacToeTile.setDrawableO(getResources().getDrawable(R.drawable.circle)); TicTacToeTile.setDrawableBlank(getResources().getDrawable(R.drawable.blank)); ArrayList<TicTacToeDrawable> tileList = new ArrayList<TicTacToeDrawable>(GAME_TILE_IDS.length); mClickHandler = new TileClickHandler(); // Set the click handler for all the tiles and build up a list for(int i=0; i<GAME_TILE_IDS.length; i++) { TicTacToeTile curTile = (TicTacToeTile) findViewById(GAME_TILE_IDS[i]); curTile.setOnClickListener(mClickHandler); tileList.add(curTile); } mEasyGameAI = new EasyGameAI(); mHardGameAI = new HardGameAI(); // Default to using the EasyGameAI mGameManager = new ClassicGameManager(tileList, mEasyGameAI); CheckBox playerGoesFirst = (CheckBox) findViewById(R.id.checkBoxPlayerFirst); if(playerGoesFirst.isChecked()) mGameManager.setStartingTurn(TicTacToeConstants.TURN_PLAYER); else mGameManager.setStartingTurn(TicTacToeConstants.TURN_COMPUTER); // Set whether the player should go first or not playerGoesFirst.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked) mGameManager.setStartingTurn(TicTacToeConstants.TURN_PLAYER); else mGameManager.setStartingTurn(TicTacToeConstants.TURN_COMPUTER); } }); // Start new easy game Button easyButton = (Button) findViewById(R.id.buttonEasyGame); easyButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Set the AI to the easy AI mGameManager.setGameAI(mEasyGameAI); mGameManager.reset(); updateStatusMessage(); } }); // Start new hard game Button hardButton = (Button) findViewById(R.id.buttonHardGame); hardButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Set the AI to the hard AI mGameManager.setGameAI(mHardGameAI); mGameManager.reset(); updateStatusMessage(); } }); // Save the text view for the status mTextViewStatus = (TextView) findViewById(R.id.textViewStatusMessage); } /** * Will set the status message based on the current game state */ public void updateStatusMessage() { // Get the games current state int gameState = mGameManager.getGameState(); Resources res = getResources(); if(gameState == TicTacToeConstants.GAME_STATE_PLAYING) { mTextViewStatus.setText(res.getString(R.string.playing_game)); } else if(gameState == TicTacToeConstants.GAME_STATE_PLAYER_WINS) { mTextViewStatus.setText(res.getString(R.string.player_wins)); } else if (gameState == TicTacToeConstants.GAME_STATE_COMPUTER_WINS) { mTextViewStatus.setText(res.getString(R.string.computer_wins)); } else if (gameState == TicTacToeConstants.GAME_STATE_CATS_GAME) { mTextViewStatus.setText(res.getString(R.string.tie_game)); } } /** * Private class to handle when a tile is clicked * @author Dominic * */ private class TileClickHandler implements OnClickListener { @Override public void onClick(View v) { if (mGameManager.getGameState() == TicTacToeConstants.GAME_STATE_PLAYING && v instanceof TicTacToeDrawable) { // Click the tile TicTacToeDrawable tile = (TicTacToeDrawable) v; if(tile.getState() == TicTacToeConstants.TILE_STATE_EMPTY) { mGameManager.playerClickedTile(tile); } // Update the status message updateStatusMessage(); } } } }
4. That's It
Screenshot of the completed game |
Thanks for taking the time to read through the Tic Tac Toe tutorial series. This is a simple version of the game that we created, but hopefully the tutorial was a great learning experience!
Thanks for your tutorial. Very useful.
ReplyDeleteMobile App Developer
August 4, 2015
check box coding
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteSuch a useful code for Android Game Development. Thanks for sharing this.
ReplyDeleteAndroid Application development