Android自定義View實現(xiàn)五子棋游戲
本文實例為大家分享了Android實現(xiàn)五子棋游戲的具體代碼,供大家參考,具體內(nèi)容如下
直接上效果圖

原理
從棋盤到棋子,到開始下棋的各類點擊事件,均在 ChessView 中實現(xiàn),這個 View 沒有提供自定義屬性(因為我覺得沒有必要~~~)。
項目GitHub地址:Wuziqi
實現(xiàn)步驟
1.新建一個棋子類,這個類非常簡單,代碼如下:
public class Chess {
public enum Color {BLACK, WHITE, NONE}
private Color color;
public Chess(){
this.color = Color.NONE;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
每個棋子類有三種狀態(tài),即 WHITE,BLACK,NONE。這里我們使用枚舉來表示這三種狀態(tài)。
2. 自定義 ChessView 類,這個類就是核心類了,我們這個五子棋的所有邏輯都是在這個類里面實現(xiàn)。構(gòu)造方法初始化各個字段,代碼如下:
public ChessView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化字段 mEveryPlay,悔棋會用到
initEveryPlay();
// 初始化每個棋子,設(shè)置屬性為 NONE
initChess();
// 初始化棋盤畫筆
initBoardPaint();
// 初始化棋子畫筆
initChessPaint();
// 初始化背景畫筆
initBgPaint();
}
各個方法的具體實現(xiàn)如下:
private void initEveryPlay() {
// 初始化 List 大小,此方法不影響 list.size() 返回值
mEveryPlay = new ArrayList<>(225);
}
private void initChess() {
mChessArray = new Chess[15][15];
for (int i = 0; i < mChessArray.length; i++) {
for (int j = 0; j < mChessArray[i].length; j++) {
mChessArray[i][j] = new Chess();
}
}
}
private void initChessPaint() {
mChessPaint = new Paint();
mChessPaint.setColor(android.graphics.Color.WHITE);
mChessPaint.setAntiAlias(true);
}
private void initBoardPaint() {
mBoardPaint = new Paint();
mBoardPaint.setColor(android.graphics.Color.BLACK);
mBoardPaint.setStrokeWidth(2);
}
private void initBgPaint() {
mBgPaint = new Paint();
mBgPaint.setColor(android.graphics.Color.GRAY);
mBgPaint.setAntiAlias(true);
}
3. 重寫 onMeasure() 方法,強制將 View 大小變?yōu)檎叫?,代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int min = widthSize < heightSize ? widthSize : heightSize;
// 五子棋標(biāo)準(zhǔn)棋盤線條數(shù)目為 15 x 15,為了后面計算坐標(biāo)方便,我們將 View 的寬高處理為 16 的整數(shù)倍
min = min / 16 * 16;
setMeasuredDimension(min, min);
}
之所以設(shè)置為 16 的整數(shù)倍而不是 15,是因為如果設(shè)置成 15,那么棋盤的背景就會跟棋盤最邊界的線條重合,此時如果有棋子落在邊界,棋子將不能顯示完全。
4. 重點來了,重寫 onDraw() 方法,繪制出棋盤,代碼如下:
@Override
protected void onDraw(Canvas canvas) {
int height = getMeasuredHeight();
int width = getMeasuredWidth();
int avg = height / 16;
canvas.drawRect(0, 0, width, height, mBgPaint);
for (int i = 1; i < 16; i++) {
// 畫豎線
canvas.drawLine(avg * i, avg, avg * i, height - avg, mBoardPaint);
// 畫橫線
canvas.drawLine(avg, avg * i, width - avg, avg * i, mBoardPaint);
}
for (int i = 1; i < 16; i++) {
for (int j = 1; j < 16; j++) {
switch (mChessArray[i - 1][j - 1].getColor()) {
case BLACK:
mChessPaint.setColor(android.graphics.Color.BLACK);
break;
case WHITE:
mChessPaint.setColor(android.graphics.Color.WHITE);
break;
case NONE:
continue;
}
canvas.drawCircle(avg * i, avg * j, avg / 2 - 0.5f, mChessPaint);
}
}
}
這樣我們就將整個棋盤畫出來了,之后我們只需要改變數(shù)組 mChessArray[][] 里面對象的 Color 屬性,再調(diào)用 invalidate() 方法便可以刷新棋盤了。
5. 接下來,我們便要處理點擊事件,實現(xiàn)對弈的邏輯了,重寫 onTouchEvent() 方法,代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 如果棋盤被鎖定(即勝負已分,返回查看棋局的時候)
// 此時只允許查看,不允許落子了
if (isLocked) {
return true;
}
float x = event.getX();
float y = event.getY();
// 以點擊的位置為中心,新建一個小矩形
Rect rect = getLittleRect(x, y);
// 獲得上述矩形包含的棋盤上的點
Point point = getContainPoint(rect);
if (point != null) {
// 若點不為空,則刷新對應(yīng)位置棋子的屬性
setChessState(point);
// 記錄下每步操作,方便悔棋操作
mEveryPlay.add(point);
if (gameIsOver(point.x, point.y)) {
// 游戲結(jié)束彈窗提示
showDialog();
}
// 更改游戲玩家
isBlackPlay = !isBlackPlay;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
下面分別來說說調(diào)用到的各個方法的實現(xiàn)思路:
getLittleRect()
/**
* 以傳入點為中心,獲得一個矩形
*
* @param x 傳入點 x 坐標(biāo)
* @param y 傳入點 y 坐標(biāo)
* @return 所得矩形
*/
private Rect getLittleRect(float x, float y) {
int side = getMeasuredHeight() / 16;
int left = (int) (x - side / 2);
int top = (int) (y - side / 2);
int right = (int) (x + side / 2);
int bottom = (int) (y + side / 2);
return new Rect(left, top, right, bottom);
}
getContainPoint()
/**
* 獲取包含在 rect 中并且是能夠下棋的位置的點
*
* @param rect 矩形
* @return 返回包含的點,若沒有包含任何點或者包含點已有棋子返回 null
*/
private Point getContainPoint(Rect rect) {
int avg = getMeasuredHeight() / 16;
for (int i = 1; i < 16; i++) {
for (int j = 1; j < 16; j++) {
if (rect.contains(avg * i, avg * j)) {
Point point = new Point(i - 1, j - 1);
// 包含點沒有棋子才返回 point
if (mChessArray[point.x][point.y].getColor() == Chess.Color.NONE) {
return point;
}
break;
}
}
}
return null;
}
showDialog()
順便一提,這個方法用的是 v7 包里面的對話框,因為這樣可以在版本較低的安卓平臺下也可以得到不錯的顯示效果,效果如下:

/**
* 游戲結(jié)束,顯示對話框
*/
private void showDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("游戲結(jié)束");
if (isBlackPlay) {
builder.setMessage("黑方獲勝?。。?);
} else {
builder.setMessage("白方獲勝?。?!");
}
builder.setCancelable(false);
builder.setPositiveButton("重新開始", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
resetChessBoard();
dialog.dismiss();
}
});
builder.setNegativeButton("返回查看", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
isLocked = true;
dialog.dismiss();
}
});
builder.show();
}
setChessState()
/**
* 重新設(shè)定用戶所點位置的棋子狀態(tài)
*
* @param point 棋子的位置
*/
private void setChessState(Point point) {
if (isBlackPlay) {
mChessArray[point.x][point.y].setColor(Chess.Color.BLACK);
} else {
mChessArray[point.x][point.y].setColor(Chess.Color.WHITE);
}
invalidate();
}
以上幾個方法都較為簡單不多說了,接下來重點講一下判斷游戲結(jié)束的邏輯。
- gameIsOver ()
/**
* 判斷游戲是否結(jié)束,游戲結(jié)束標(biāo)志:當(dāng)前落子位置與其他同色棋子連成 5 個
*
* @param x 落子位置 x 坐標(biāo)
* @param y 落子位置 y 坐標(biāo)
* @return 若連成 5 個,游戲結(jié)束,返回 true,負責(zé)返回 false
*/
private boolean gameIsOver(int x, int y) {
Chess.Color color = mChessArray[x][y].getColor();
return isOverA(x, y, color) || isOverB(x, y, color) || isOverC(x, y, color) || isOverD(x, y, color);
}
這個方法用來判斷游戲是否結(jié)束,思路便是以當(dāng)前落子位置為基準(zhǔn),去尋找豎直、水平、左上至右下、左下至右上四個方向是否連成 5 子,分別對應(yīng) isOverA(), isOverB(), isOverC(), isOverD() 四個方法,這四個方法的實現(xiàn)如下:
private boolean isOverA(int x, int y, Chess.Color color) {
int amount = 0;
for (int i = y; i >= 0; i--) {
if (mChessArray[x][i].getColor() == color) {
amount++;
} else {
break;
}
}
for (int i = y; i < mChessArray[x].length; i++) {
if (mChessArray[x][i].getColor() == color) {
amount++;
} else {
break;
}
}
// 循環(huán)執(zhí)行完成后,當(dāng)前落子位置算了兩次,故條件應(yīng)是大于 5
return amount > 5;
}
private boolean isOverB(int x, int y, Chess.Color color) {
int amount = 0;
for (int i = x; i >= 0; i--) {
if (mChessArray[i][y].getColor() == color) {
amount++;
} else {
break;
}
}
for (int i = x; i < mChessArray.length; i++) {
if (mChessArray[i][y].getColor() == color) {
amount++;
} else {
break;
}
}
// 循環(huán)執(zhí)行完成后,當(dāng)前落子位置算了兩次,故條件應(yīng)是大于 5
return amount > 5;
}
private boolean isOverC(int x, int y, Chess.Color color) {
int amount = 0;
for (int i = x, j = y; i >= 0 && j >= 0; i--, j--) {
if (mChessArray[i][j].getColor() == color) {
amount++;
} else {
break;
}
}
for (int i = x, j = y; i < mChessArray.length && j < mChessArray[i].length; i++, j++) {
if (mChessArray[i][j].getColor() == color) {
amount++;
} else {
break;
}
}
// 循環(huán)執(zhí)行完成后,當(dāng)前落子位置算了兩次,故條件應(yīng)是大于 5
return amount > 5;
}
private boolean isOverD(int x, int y, Chess.Color color) {
int amount = 0;
for (int i = x, j = y; i < mChessArray.length && j >= 0; i++, j--) {
if (mChessArray[i][j].getColor() == color) {
amount++;
} else {
break;
}
}
for (int i = x, j = y; i >= 0 && j < mChessArray[i].length; i--, j++) {
if (mChessArray[i][j].getColor() == color) {
amount++;
} else {
break;
}
}
// 循環(huán)執(zhí)行完成后,當(dāng)前落子位置算了兩次,故條件應(yīng)是大于 5
return amount > 5;
}
6. 最后定義兩個公有方法,方便 Activity 調(diào)用,用來執(zhí)行悔棋和重置棋盤操作。兩個方法代碼如下:
/**
* 悔棋,實現(xiàn)思路為:記錄每一步走棋的坐標(biāo),若點擊了悔棋,
* 則拿出最后記錄的坐標(biāo),對 mChessArray 里面對應(yīng)坐標(biāo)的
* 棋子進行處理(設(shè)置顏色為 NONE),并移除集合里面最后
* 一個元素
*/
public void retract() {
if (mEveryPlay.isEmpty()) {
return;
}
Point point = mEveryPlay.get(mEveryPlay.size() - 1);
mChessArray[point.x][point.y].setColor(Chess.Color.NONE);
mEveryPlay.remove(mEveryPlay.size() - 1);
isLocked = false;
isBlackPlay = !isBlackPlay;
invalidate();
}
/**
* 重置棋盤
*/
public void resetChessBoard() {
for (Chess[] chessRow : mChessArray) {
for (Chess chess : chessRow) {
chess.setColor(Chess.Color.NONE);
}
}
mEveryPlay.clear();
isBlackPlay = true;
isLocked = false;
invalidate();
}
到此, ChessView 已經(jīng)寫完了,接下來只要在布局文件里面聲明即可。
7. 在 activity_main 布局文件如下,非常簡單,我相信不用多說都能看懂:
<?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:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <com.yangqi.wuziqi.ChessView android:id="@+id/chessView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerHorizontal="true"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/chessView" android:layout_margin="20dp" android:gravity="center_horizontal" android:orientation="horizontal"> <Button android:id="@+id/bt_retract" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="悔棋" /> <Button android:id="@+id/bt_reset" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="重新開始"/> </LinearLayout> </RelativeLayout>
8. 最后一步了,只需要在 MainActivity 里面拿到 ChessView 對象和兩個 Button 按鈕,即可實現(xiàn)悔棋與重新開始:
package com.yangqi.wuziqi;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button bt_reset;
private Button bt_retract;
private ChessView chessView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initUI();
initListener();
}
private void initListener() {
bt_reset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
chessView.resetChessBoard();
}
});
bt_retract.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
chessView.retract();
}
});
}
private void initUI() {
bt_reset = (Button) findViewById(R.id.bt_reset);
bt_retract = (Button) findViewById(R.id.bt_retract);
chessView = (ChessView) findViewById(R.id.chessView);
}
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android滑動刪除數(shù)據(jù)功能的實現(xiàn)代碼
這篇文章主要介紹了Android滑動刪除功能2017-01-01
Android 實現(xiàn)圖片轉(zhuǎn)二進制流及二進制轉(zhuǎn)字符串
這篇文章主要介紹了Android 實現(xiàn)圖片轉(zhuǎn)二進制流及二進制轉(zhuǎn)字符串,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android基于Mapbox?V10?繪制LineGradient軌跡
這篇文章主要介紹了Android基于Mapbox?V10?繪制LineGradient軌跡,文章通告介紹一些V10上的用法,最終講下如何繪制漸變運動記錄軌跡,感興趣的小伙伴可以參考一下2022-08-08
Android ListView適配器(Adapter)優(yōu)化方法詳解
這篇文章主要介紹了Android ListView優(yōu)化方法詳解的相關(guān)資料,這里舉例說明該如何對ListView 進行優(yōu)化,具有一定的參考價值,需要的朋友可以參考下2016-11-11
Android?SeekBar充當(dāng)Progress實現(xiàn)兔兔進度條Plus
這篇文章主要為大家介紹了Android?SeekBar充當(dāng)Progress實現(xiàn)兔兔進度條Plus示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
Android編程中聊天頁面背景圖片、標(biāo)題欄由于鍵盤引起問題的解決方法
這篇文章主要介紹了Android編程中聊天頁面背景圖片、標(biāo)題欄由于鍵盤引起問題的解決方法,針對鍵盤彈出時標(biāo)題欄及背景圖片異常的相關(guān)解決方法,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10
Android報錯Error:Could not find com.android.tools.build:gradle
這篇文章主要介紹了Android Studio報錯Error:Could not find com.android.tools.build:gradle:4.1解決辦法,碰到該問題的同學(xué)快過來看看吧2021-08-08
Android RefreshLayout實現(xiàn)下拉刷新布局
這篇文章主要為大家詳細介紹了Android RefreshLayout實現(xiàn)下拉刷新布局,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10

