Android自定义View实现五子棋游戏
人气:0直接上效果图
原理
从棋盘到棋子,到开始下棋的各类点击事件,均在 ChessView 中实现,这个 View 没有提供自定义属性(因为我觉得没有必要~~~)。
项目GitHub地址:Wuziqi
实现步骤
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; } }
每个棋子类有三种状态,即 WHITE,BLACK,NONE。这里我们使用枚举来表示这三种状态。
2. 自定义 ChessView 类,这个类就是核心类了,我们这个五子棋的所有逻辑都是在这个类里面实现。构造方法初始化各个字段,代码如下:
public ChessView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 初始化字段 mEveryPlay,悔棋会用到 initEveryPlay(); // 初始化每个棋子,设置属性为 NONE initChess(); // 初始化棋盘画笔 initBoardPaint(); // 初始化棋子画笔 initChessPaint(); // 初始化背景画笔 initBgPaint(); }
各个方法的具体实现如下:
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 大小变为正方形,代码如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int min = widthSize < heightSize ? widthSize : heightSize; // 五子棋标准棋盘线条数目为 15 x 15,为了后面计算坐标方便,我们将 View 的宽高处理为 16 的整数倍 min = min / 16 * 16; setMeasuredDimension(min, min); }
之所以设置为 16 的整数倍而不是 15,是因为如果设置成 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); } } }
这样我们就将整个棋盘画出来了,之后我们只需要改变数组 mChessArray[][] 里面对象的 Color 属性,再调用 invalidate() 方法便可以刷新棋盘了。
5. 接下来,我们便要处理点击事件,实现对弈的逻辑了,重写 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) { // 若点不为空,则刷新对应位置棋子的属性 setChessState(point); // 记录下每步操作,方便悔棋操作 mEveryPlay.add(point); if (gameIsOver(point.x, point.y)) { // 游戏结束弹窗提示 showDialog(); } // 更改游戏玩家 isBlackPlay = !isBlackPlay; } break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } return super.onTouchEvent(event); }
下面分别来说说调用到的各个方法的实现思路:
getLittleRect()
/** * 以传入点为中心,获得一个矩形 * * @param x 传入点 x 坐标 * @param y 传入点 y 坐标 * @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 包里面的对话框,因为这样可以在版本较低的安卓平台下也可以得到不错的显示效果,效果如下:
/** * 游戏结束,显示对话框 */ private void showDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle("游戏结束"); 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()
/** * 重新设定用户所点位置的棋子状态 * * @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(); }
以上几个方法都较为简单不多说了,接下来重点讲一下判断游戏结束的逻辑。
- gameIsOver ()
/** * 判断游戏是否结束,游戏结束标志:当前落子位置与其他同色棋子连成 5 个 * * @param x 落子位置 x 坐标 * @param y 落子位置 y 坐标 * @return 若连成 5 个,游戏结束,返回 true,负责返回 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); }
这个方法用来判断游戏是否结束,思路便是以当前落子位置为基准,去寻找竖直、水平、左上至右下、左下至右上四个方向是否连成 5 子,分别对应 isOverA(), isOverB(), isOverC(), isOverD() 四个方法,这四个方法的实现如下:
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; } } // 循环执行完成后,当前落子位置算了两次,故条件应是大于 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; } } // 循环执行完成后,当前落子位置算了两次,故条件应是大于 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; } } // 循环执行完成后,当前落子位置算了两次,故条件应是大于 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; } } // 循环执行完成后,当前落子位置算了两次,故条件应是大于 5 return amount > 5; }
6. 最后定义两个公有方法,方便 Activity 调用,用来执行悔棋和重置棋盘操作。两个方法代码如下:
/** * 悔棋,实现思路为:记录每一步走棋的坐标,若点击了悔棋, * 则拿出最后记录的坐标,对 mChessArray 里面对应坐标的 * 棋子进行处理(设置颜色为 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 已经写完了,接下来只要在布局文件里面声明即可。
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 按钮,即可实现悔棋与重新开始:
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); } }
您可能感兴趣的文章:
加载全部内容