Android自定義View實(shí)現(xiàn)五子棋游戲
本文實(shí)例為大家分享了Android實(shí)現(xiàn)五子棋游戲的具體代碼,供大家參考,具體內(nèi)容如下
直接上效果圖
原理
從棋盤到棋子,到開始下棋的各類點(diǎn)擊事件,均在 ChessView 中實(shí)現(xiàn),這個(gè) View 沒有提供自定義屬性(因?yàn)槲矣X得沒有必要~~~)。
項(xiàng)目GitHub地址:Wuziqi
實(shí)現(xiàn)步驟
1.新建一個(gè)棋子類,這個(gè)類非常簡(jiǎn)單,代碼如下:
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; } }
每個(gè)棋子類有三種狀態(tài),即 WHITE,BLACK,NONE。這里我們使用枚舉來(lái)表示這三種狀態(tài)。
2. 自定義 ChessView 類,這個(gè)類就是核心類了,我們這個(gè)五子棋的所有邏輯都是在這個(gè)類里面實(shí)現(xiàn)。構(gòu)造方法初始化各個(gè)字段,代碼如下:
public ChessView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 初始化字段 mEveryPlay,悔棋會(huì)用到 initEveryPlay(); // 初始化每個(gè)棋子,設(shè)置屬性為 NONE initChess(); // 初始化棋盤畫筆 initBoardPaint(); // 初始化棋子畫筆 initChessPaint(); // 初始化背景畫筆 initBgPaint(); }
各個(gè)方法的具體實(shí)現(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() 方法,強(qiáng)制將 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,為了后面計(jì)算坐標(biāo)方便,我們將 View 的寬高處理為 16 的整數(shù)倍 min = min / 16 * 16; setMeasuredDimension(min, min); }
之所以設(shè)置為 16 的整數(shù)倍而不是 15,是因?yàn)槿绻O(shè)置成 15,那么棋盤的背景就會(huì)跟棋盤最邊界的線條重合,此時(shí)如果有棋子落在邊界,棋子將不能顯示完全。
4. 重點(diǎn)來(lái)了,重寫 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); } } }
這樣我們就將整個(gè)棋盤畫出來(lái)了,之后我們只需要改變數(shù)組 mChessArray[][] 里面對(duì)象的 Color 屬性,再調(diào)用 invalidate() 方法便可以刷新棋盤了。
5. 接下來(lái),我們便要處理點(diǎn)擊事件,實(shí)現(xiàn)對(duì)弈的邏輯了,重寫 onTouchEvent() 方法,代碼如下:
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 如果棋盤被鎖定(即勝負(fù)已分,返回查看棋局的時(shí)候) // 此時(shí)只允許查看,不允許落子了 if (isLocked) { return true; } float x = event.getX(); float y = event.getY(); // 以點(diǎn)擊的位置為中心,新建一個(gè)小矩形 Rect rect = getLittleRect(x, y); // 獲得上述矩形包含的棋盤上的點(diǎn) Point point = getContainPoint(rect); if (point != null) { // 若點(diǎn)不為空,則刷新對(duì)應(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); }
下面分別來(lái)說說調(diào)用到的各個(gè)方法的實(shí)現(xiàn)思路:
getLittleRect()
/** * 以傳入點(diǎn)為中心,獲得一個(gè)矩形 * * @param x 傳入點(diǎn) x 坐標(biāo) * @param y 傳入點(diǎn) 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 中并且是能夠下棋的位置的點(diǎn) * * @param rect 矩形 * @return 返回包含的點(diǎn),若沒有包含任何點(diǎn)或者包含點(diǎn)已有棋子返回 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); // 包含點(diǎn)沒有棋子才返回 point if (mChessArray[point.x][point.y].getColor() == Chess.Color.NONE) { return point; } break; } } } return null; }
showDialog()
順便一提,這個(gè)方法用的是 v7 包里面的對(duì)話框,因?yàn)檫@樣可以在版本較低的安卓平臺(tái)下也可以得到不錯(cuò)的顯示效果,效果如下:
/** * 游戲結(jié)束,顯示對(duì)話框 */ 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è)定用戶所點(diǎn)位置的棋子狀態(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(); }
以上幾個(gè)方法都較為簡(jiǎn)單不多說了,接下來(lái)重點(diǎn)講一下判斷游戲結(jié)束的邏輯。
- gameIsOver ()
/** * 判斷游戲是否結(jié)束,游戲結(jié)束標(biāo)志:當(dāng)前落子位置與其他同色棋子連成 5 個(gè) * * @param x 落子位置 x 坐標(biāo) * @param y 落子位置 y 坐標(biāo) * @return 若連成 5 個(gè),游戲結(jié)束,返回 true,負(fù)責(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); }
這個(gè)方法用來(lái)判斷游戲是否結(jié)束,思路便是以當(dāng)前落子位置為基準(zhǔn),去尋找豎直、水平、左上至右下、左下至右上四個(gè)方向是否連成 5 子,分別對(duì)應(yīng) isOverA(), isOverB(), isOverC(), isOverD() 四個(gè)方法,這四個(gè)方法的實(shí)現(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. 最后定義兩個(gè)公有方法,方便 Activity 調(diào)用,用來(lái)執(zhí)行悔棋和重置棋盤操作。兩個(gè)方法代碼如下:
/** * 悔棋,實(shí)現(xiàn)思路為:記錄每一步走棋的坐標(biāo),若點(diǎn)擊了悔棋, * 則拿出最后記錄的坐標(biāo),對(duì) mChessArray 里面對(duì)應(yīng)坐標(biāo)的 * 棋子進(jìn)行處理(設(shè)置顏色為 NONE),并移除集合里面最后 * 一個(gè)元素 */ 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)寫完了,接下來(lái)只要在布局文件里面聲明即可。
7. 在 activity_main 布局文件如下,非常簡(jiǎn)單,我相信不用多說都能看懂:
<?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 對(duì)象和兩個(gè) Button 按鈕,即可實(shí)現(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); } }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android實(shí)現(xiàn)五子棋游戲(局域網(wǎng)版)
- android自定義View實(shí)現(xiàn)簡(jiǎn)單五子棋游戲
- Android自定義view之圍棋動(dòng)畫效果的實(shí)現(xiàn)
- 基于android實(shí)現(xiàn)五子棋開發(fā)
- Android自定義View實(shí)現(xiàn)五子棋游戲
- Android自定義View實(shí)現(xiàn)五子棋小游戲
- android簡(jiǎn)單自定義View實(shí)現(xiàn)五子棋
- Android開發(fā)實(shí)現(xiàn)的簡(jiǎn)單五子棋游戲示例
- Android游戲開發(fā)之黑白棋
- Android實(shí)現(xiàn)中國(guó)象棋游戲(局域網(wǎng)版)
相關(guān)文章
Android滑動(dòng)刪除數(shù)據(jù)功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android滑動(dòng)刪除功能2017-01-01Android中制作自定義dialog對(duì)話框的實(shí)例分享
這篇文章主要介紹了Android中制作自定義dialog對(duì)話框的實(shí)例分享,安卓自帶的Dialog顯然不夠用,因而我們要繼承Dialog類來(lái)制作自己的對(duì)話框,需要的朋友可以參考下2016-04-04Android 實(shí)現(xiàn)圖片轉(zhuǎn)二進(jìn)制流及二進(jìn)制轉(zhuǎn)字符串
這篇文章主要介紹了Android 實(shí)現(xiàn)圖片轉(zhuǎn)二進(jìn)制流及二進(jìn)制轉(zhuǎn)字符串,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2020-03-03Android基于Mapbox?V10?繪制LineGradient軌跡
這篇文章主要介紹了Android基于Mapbox?V10?繪制LineGradient軌跡,文章通告介紹一些V10上的用法,最終講下如何繪制漸變運(yùn)動(dòng)記錄軌跡,感興趣的小伙伴可以參考一下2022-08-08Android ListView適配器(Adapter)優(yōu)化方法詳解
這篇文章主要介紹了Android ListView優(yōu)化方法詳解的相關(guān)資料,這里舉例說明該如何對(duì)ListView 進(jìn)行優(yōu)化,具有一定的參考價(jià)值,需要的朋友可以參考下2016-11-11Android?SeekBar充當(dāng)Progress實(shí)現(xiàn)兔兔進(jìn)度條Plus
這篇文章主要為大家介紹了Android?SeekBar充當(dāng)Progress實(shí)現(xiàn)兔兔進(jìn)度條Plus示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android編程中聊天頁(yè)面背景圖片、標(biāo)題欄由于鍵盤引起問題的解決方法
這篇文章主要介紹了Android編程中聊天頁(yè)面背景圖片、標(biāo)題欄由于鍵盤引起問題的解決方法,針對(duì)鍵盤彈出時(shí)標(biāo)題欄及背景圖片異常的相關(guān)解決方法,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android報(bào)錯(cuò)Error:Could not find com.android.tools.build:gradle
這篇文章主要介紹了Android Studio報(bào)錯(cuò)Error:Could not find com.android.tools.build:gradle:4.1解決辦法,碰到該問題的同學(xué)快過來(lái)看看吧2021-08-08Android RefreshLayout實(shí)現(xiàn)下拉刷新布局
這篇文章主要為大家詳細(xì)介紹了Android RefreshLayout實(shí)現(xiàn)下拉刷新布局,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10