最近一直在學習Android自定義View方面的知識,正好看到一個講解制作五子棋小游戲的案例,遂學習一番,記錄下學習過程,幫助那些有需要的人。
首先放上效果圖:
下面我將帶領大家一步步完成這個五子棋小游戲。
一、創建自定義View類及定義成員變量
首先我們先定義一個類
WuziqiPanel
,讓該類繼承自View,并在類中定義一些成員變量,便于我們后面使用,而且在我們需要顯示五子棋的布局文件中引入該自定義View。
WuziqiPanel.java
文件
public class WuziqiPanel extends View{
//棋盤寬度
private int mPanelWidth;
//棋盤格子的行高(聲明為int會造成由于不能整除而造成的誤差較大)
private float mLineHeight;
//棋盤最大行列數(其實就是棋盤橫豎線的個數)
private int MAX_LINE_NUM = 10;
//定義畫筆繪制棋盤格子
private Paint mPaint = new Paint();
//定義黑白棋子Bitmap
private Bitmap mWhitePiece;
private Bitmap mBlackPiece;
//棋子的縮放比例(行高的3/4)
private float pieceScaleRatio = 3 * 1.0f / 4;
//存儲黑白棋子的坐標
private ArrayList<Point> mWhiteArray = new ArrayList<>();
private ArrayList<Point> mBlackArray = new ArrayList<>();
//哪方先下子
private boolean isWhiteFirst = true;
//游戲是否結束
private boolean isGameOver;
//確定贏家
private boolean isWhiteWinner = false;
//游戲結束監聽
private OnGameOverListener onGameOverListener;
}
在
activity_main.xml
文件中引入該自定義View
activity_main.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/main_bg"
tools:context="com.codekong.wuziqi.activity.MainActivity">
<com.codekong.wuziqi.view.WuziqiPanel
android:id="@+id/id_wuziqi_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
</RelativeLayout>
注意:自定義View的引入必須使用完整路徑
二、定義構造函數并初始化設置
這一步我們首先要書寫構造函數,并且在構造函數中初始化一些設置。比如初始化畫筆以及將棋子圖片轉為bitmap
這里我們在三個參數的構造方法中調用兩個參數的構造方法,又在兩個參數的構造方法中調用一個參數的構造方法。
這里簡單解釋一下。
一個參數的構造方法
是我們在new
出一個組件的時候調用;兩個參數的構造方法
是我們在XML中使用自定義View時調用;三個參數的構造方法
是我們自定義View中使用了自定義屬性的時候調用;所以我們按上面的寫法就可以覆蓋到這三種情況。
public WuziqiPanel(Context context) {
this(context, null);
}
public WuziqiPanel(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WuziqiPanel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化設置
*/
private void init() {
//初始化畫筆
mPaint.setColor(0x88000000);
//設置抗鋸齒
mPaint.setAntiAlias(true);
//設置防抖動
mPaint.setDither(true);
//設置為空心(畫線)
mPaint.setStyle(Paint.Style.STROKE);
//初始化棋子
mWhitePiece = BitmapFactory.decodeResource(getResources(), R.drawable.icon_white_piece);
mBlackPiece = BitmapFactory.decodeResource(getResources(), R.drawable.icon_black_piece);
}
三、測量
測量幾乎是自定義View必須要經歷的步驟,由于我們要先繪制棋盤,所以我們必須先測量出我們需要的數據。
我們在onMeasure()
方法中拿到屏幕寬高,然后在onSizeChanged()
中獲得棋盤的寬度,計算出棋盤的行高。接著根據行高縮放棋子大小,使其顯示大小合適。
/**
* 測量
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = Math.min(widthSize, heightSize);
//此處的邏輯判斷是處理當我們自定義的View被嵌套在ScrollView中時,獲得的測量模式
// 會是UNSPECIFIED
// 使得到的widthSize或者heightSize為0
if (widthMode == MeasureSpec.UNSPECIFIED){
width = heightSize;
}else if (heightMode == MeasureSpec.UNSPECIFIED){
width = widthSize;
}
//調用此方法使我們的測量結果生效
setMeasuredDimension(width, width);
}
/**
* 當寬高發生變化時回調此方法
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//此處的參數w就是在onMeasure()方法中設置的自定義View的大小
//計算出棋盤寬度和行高
mPanelWidth = w;
mLineHeight = mPanelWidth * 1.0f / MAX_LINE_NUM;
//將棋子根據行高變化
int pieceWidth = (int) (pieceScaleRatio * mLineHeight);
mWhitePiece = Bitmap.createScaledBitmap(mWhitePiece, pieceWidth, pieceWidth, false);
mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, pieceWidth, pieceWidth, false);
}
注意:如上面注釋所寫,由于我們不知道我們的自定義View將會被放在什么樣的布局中,所以如果我們的五子棋盤被放在ScrollView中,我們測量到的寬或者高就會有一方為,就會使我們的測量失效,從而影響后面的繪制,所以我們必須處理這一種情況.
四、繪制棋盤
首先我們應該先繪制好我們要下棋的棋盤,我們此處準備橫豎都畫10條線來繪制我們的棋盤,其實此處的棋盤的橫豎線的個數我們是可以修改的,棋子的大小也會隨著棋盤格子的大小縮放,但是為了美觀一些,我們此處采用橫豎10條線.
大家可以先通過我下面的示意圖來理解一下棋盤橫豎線坐標的確定.
/**
* 繪制棋盤
* @param canvas
*/
private void drawBoard(Canvas canvas) {
int w = mPanelWidth;
float lineHeight = mLineHeight;
for (int i = 0; i < MAX_LINE_NUM; i++) {
int startX = (int) (lineHeight / 2);
int endX = (int) (w - lineHeight / 2);
int y = (int) ((0.5 + i) * lineHeight);
//畫橫線
canvas.drawLine(startX, y, endX, y, mPaint);
//畫豎線
canvas.drawLine(y, startX, y, endX, mPaint);
}
}
通過上面的步驟棋盤就算是繪制好了.
五、處理用戶手勢-下棋
上面我們繪制好了棋盤,接著我們就可以下棋啦,所以理所當然我們要開始處理用戶的手勢,開始下棋啦,所以我們要重寫
onTouchEvent()
.
這一步我們要做三件事:
1 . onTouchEvent()
中 return true
攔截手勢事件我們自己處理
2 . 獲得用戶觸摸的坐標并進行處理,處理為棋盤上的整數值坐標,并存儲起來.
這一步我們調用一個自定義的函數
getValidPoint()
將用戶點擊的點的坐標轉化為整數值.也就是說用戶下子的時候不必精確點擊到棋盤各自的交叉點上,而是只要在這個交叉點周圍就可以了,我們只需要將其取整后除以我們格子的高度(行高),
簡單解釋一下,如下圖1、2所指的箭頭所示,由于我們的棋盤上下左右邊距為0.5倍的行高,當我們手指在以棋盤頂點0.5倍的行高范圍內點擊,只要除以行高取整,就會得到該頂點坐標.比如我點擊的坐標點為(0.75,0.82),在第一個圈范圍內,此時行高為,除以1后取整得到(0,0)就是棋盤的頂點,就是我們需要落子的地方。
/**
* 將用戶點擊的位置的Point轉換為類似于(0,0)d的坐標
* @param x
* @param y
* @return
*/
private Point getValidPoint(int x, int y) {
return new Point((int) (x / mLineHeight), (int) (y / mLineHeight));
}
3 . 調用invalidate()
方法進行界面重繪,繪制出用戶所下的棋子
每次調用
invalidate()
方法就會調用onDraw()
方法進行界面繪制,在該方法中先繪制棋盤,然后繪制用戶所下的棋子,還要判斷游戲結束.繪制棋子和判斷游戲結束我會在后面的步驟中給出介紹.
4 . 將下棋權利交于另一方(白棋下完換黑棋)
這一步比較好處理,我們只需要將一個布爾變量
isWhiteFirst
取反,下一次就輪到另一種棋子下子了.
/**
* 處理用戶手勢操作
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isGameOver) return false;
int action = event.getAction();
//手指抬起后處理
if (action == MotionEvent.ACTION_UP){
//攔截事件自己來處理
int x = (int) event.getX();
int y = (int) event.getY();
Point point = getValidPoint(x, y);
//首先判斷所點擊的位置是不是已經有棋子
if (mWhiteArray.contains(point) || mBlackArray.contains(point)){
return false;
}
//白棋先下
if (isWhiteFirst){
mWhiteArray.add(point);
}else{
mBlackArray.add(point);
}
//調用重繪
invalidate();
isWhiteFirst = !isWhiteFirst;
}
return true;
}
/**
* 進行繪制工作
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制棋盤
drawBoard(canvas);
//繪制用戶已經下的所有棋子
drawPieces(canvas);
//判斷游戲是否結束
checkGameOver();
}
六、繪制棋子
上一步我們已經把用戶所點擊的要下棋子的坐標存儲在了ArrayList,這一步我們就將遍歷這個ArrayList將黑白棋子繪制到棋盤上.
這里我們的一個變量
pieceScaleRatio = 3/4
,表示一個棋子長寬為3/4的行高,剩余的1/4的行高平均棋子的左右各留出1/8的行高,這樣棋子距離左右邊框的距離為1/8行高,棋子與棋子之間的間距為2*1/8=1/4行高.
/**
* 繪制棋子
* @param canvas
*/
private void drawPieces(Canvas canvas) {
//繪制白棋子
for (int i = 0, n = mWhiteArray.size(); i < n; i++) {
Point whitePoint = mWhiteArray.get(i);
//棋子之間的間隔為1/4行高
canvas.drawBitmap(mWhitePiece,
(whitePoint.x + (1 - pieceScaleRatio) / 2) * mLineHeight,
(whitePoint.y + (1 - pieceScaleRatio) / 2) * mLineHeight, null);
}
//繪制黑棋子
for (int i = 0, n = mBlackArray.size(); i < n; i++) {
Point blackPoint = mBlackArray.get(i);
//棋子之間的間隔為1/4行高,棋子距離左右邊框的距離為1/8行高
canvas.drawBitmap(mBlackPiece,
(blackPoint.x + (1 - pieceScaleRatio) / 2) * mLineHeight,
(blackPoint.y + (1 - pieceScaleRatio) / 2) * mLineHeight, null);
}
}
七、判斷游戲結束
經過上面的步驟,我們已經可以在棋盤上落子了,下面的任務就是我們要判斷游戲是否結束.
這個游戲的規則比較簡單,我們只要判斷在上下左右斜對角線如果存在連續5個棋子是同一色,我們就可以判定勝負了.
我們專門定義一個工具類WuziqiUtil.java
來進行判斷
public class WuziqiUtil {
//每行上最大的數目
public static final int MAX_COUNT_IN_LINE = 5;
/**
* 檢查是否五子連珠
* @param points
* @return
*/
public static boolean checkFiveInLine(List<Point> points) {
for (Point p: points) {
int x = p.x;
int y = p.y;
boolean win = checkHorizontal(x, y, points);
if (win) return true;
win = checkVertical(x, y, points);
if (win) return true;
win = checkLeftDiagonal(x, y, points);
if (win) return true;
win = checkRightDiagonal(x, y, points);
if (win) return true;
}
return false;
}
/**
* 判斷x, y位置的棋子是否橫向五個一致
* @param x
* @param y
* @param points
* @return
*/
public static boolean checkHorizontal(int x, int y, List<Point> points) {
int count = 1;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x - i, y))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x + i, y))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
return false;
}
/**
* 判斷x, y位置的棋子是否豎向五個一致
* @param x
* @param y
* @param points
* @return
*/
public static boolean checkVertical(int x, int y, List<Point> points) {
int count = 1;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x, y - i))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x, y + i))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
return false;
}
/**
* 判斷x, y位置的棋子是否左斜向上五個一致
* @param x
* @param y
* @param points
* @return
*/
public static boolean checkLeftDiagonal(int x, int y, List<Point> points) {
int count = 1;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x - i, y + i))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x + i, y - i))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
return false;
}
/**
* 判斷x, y位置的棋子是否右斜向下五個一致
* @param x
* @param y
* @param points
* @return
*/
public static boolean checkRightDiagonal(int x, int y, List<Point> points) {
int count = 1;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x - i, y - i))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {
if (points.contains(new Point(x + i, y + i))){
count ++;
}else {
break;
}
}
if (count == MAX_COUNT_IN_LINE) return true;
return false;
}
}
然后我們只要在在自定義View類中的
checkGameOver()
方法中進行調用就可以判斷游戲是否結束
/**
* 檢查游戲是否結束
*/
private void checkGameOver() {
//檢查是否五子連珠
boolean whiteWin = WuziqiUtil.checkFiveInLine(mWhiteArray);
boolean blackWin = WuziqiUtil.checkFiveInLine(mBlackArray);
if (whiteWin || blackWin){
isGameOver = true;
isWhiteWinner = whiteWin;
//String msg = isWhiteWinner ? "白子獲勝" : "黑子獲勝";
//Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();
onGameOverListener.gameOver(isWhiteWinner);
}
}
或許已經有人發現了,我上面還聲明了一個游戲結束的監聽,對的,我們作為一個自定義View當然要將游戲的勝負結果通過回調函數返回給使用者,讓其自行處理.
/**
* 游戲結束回調監聽
*/
public interface OnGameOverListener{
void gameOver(boolean isWhiterWinner);
}
/**
* 設置游戲結束回調監聽
* @param onGameOverListener
*/
public void setOnGameOverListener(OnGameOverListener onGameOverListener){
this.onGameOverListener = onGameOverListener;
}
到這里看起來我們的五子棋小游戲已經開發完成了,但是一個負責的程序員怎么可能滿足于此呢,我們還要讓我們的游戲更健壯.
八、防止游戲被回收
我們想象一個場景,當我們正在玩五子棋小游戲,正到關鍵時刻,來電話了,這時候我們去接電話,這時候我們的小游戲就相當于處于后臺,假如這時候手機內存不足,那我們在后臺的小游戲就可能被內存回收了,當我們打完電話,發現棋盤上一個棋子都沒啦,是不是很傷心,為了解決這個問題,我們就需要重寫
onSaveInstanceState()
方法和onRestoreInstanceState()
方法來保存和恢復我們的游戲狀態.
我們可以通過旋轉屏幕模擬出上面提到的情況,旋轉屏幕就會觸發上面兩個函數.
/**
* 防止內存不足活動被回收
*/
private static final String INSTANCE = "instance";
private static final String INSTANCE_GAME_OVER = "instance_game_over";
private static final String INSTANCE_WHITE_ARRAY = "instance_white_array";
private static final String INSTANCE_BLACK_ARRAY = "instance_black_array";
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(INSTANCE, super.onSaveInstanceState());
bundle.putBoolean(INSTANCE_GAME_OVER, isGameOver);
bundle.putParcelableArrayList(INSTANCE_WHITE_ARRAY, mWhiteArray);
bundle.putParcelableArrayList(INSTANCE_BLACK_ARRAY, mBlackArray);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle){
Bundle bundle = (Bundle) state;
isGameOver = bundle.getBoolean(INSTANCE_GAME_OVER);
mWhiteArray = bundle.getParcelableArrayList(INSTANCE_WHITE_ARRAY);
mBlackArray = bundle.getParcelableArrayList(INSTANCE_BLACK_ARRAY);
super.onRestoreInstanceState(bundle.getParcelable(INSTANCE));
return;
}
super.onRestoreInstanceState(state);
}
好了,這樣我們的游戲就健壯了不少.
九、再來一局
一個游戲怎么可以只玩一次呢,所以我們這里還需要向使用者保留一個再來一局的方法.
/**
* 重新開始,再來一局
*/
public void restart(){
mBlackArray.clear();
mWhiteArray.clear();
isGameOver = false;
isWhiteWinner = false;
//重繪
invalidate();
}
十、使用該自定義View
定義已經全部完成了,現在使用就非常簡單了.
activity_main.xml
文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/main_bg"
tools:context="com.codekong.wuziqi.activity.MainActivity">
<com.codekong.wuziqi.view.WuziqiPanel
android:id="@+id/id_wuziqi_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
</RelativeLayout>
MainActivity.java
文件
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WuziqiPanel panel = (WuziqiPanel) findViewById(R.id.id_wuziqi_panel);
panel.setOnGameOverListener(new WuziqiPanel.OnGameOverListener() {
@Override
public void gameOver(boolean isWhiterWinner) {
//處理勝負結果
}
});
}
}
十一、結語
游戲的介紹就到此為止了,希望可以幫助到需要的人.
源代碼已經在Github開源,開源地址: https://github.com/codekongs/WuZiQi
歡迎大家start和fork
下集預告:五子棋小游戲之AI篇,通過算法實現人機對戰。