Android自定义View实现五子棋
首先看下实际效果
实现的思路
-
绘制棋盘
-
绘制已经存在的棋子
-
监听触摸事件,判断落子
-
判断胜利条件
重写
onDraw
方法如下@Override protected void onDraw(Canvas canvas) { //绘制棋盘 drawBoard(canvas); //添加棋盘上的点 addPoints(); if (!isGameOver) { //绘制随手指滑动的棋子 drawFreeChessMans(canvas); } //绘制棋子 drawChessMans(canvas); }
实现的步骤
绘制棋盘
-
创建构造方法,初始化部分变量
private Map<String, ChessPoint> chessPoints; private Paint paint; private TextPaint textPaint; public ChessboardView(Context context) { this(context, null); } public ChessboardView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public ChessboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, @Nullable AttributeSet attrs) { paint = new Paint(); textPaint = new TextPaint(); paint.setAntiAlias(true); textPaint.setAntiAlias(true); chessPoints = new HashMap<>(15 * 15); }
-
由于五子棋盘是正方形的,所以我们重写
onMeasure
方法强制让ChessboardView
变成正方形@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int min = widthSize < heightSize ? widthSize : heightSize; setMeasuredDimension(min, min); }
-
绘制棋盘(这里我找了百度百科五子棋下面的图片作为参考).可以看到:
A,棋盘是横竖各有15条线;
B,棋盘左边右边有数字/字母的标志;
C,棋盘有D4,L4,D12,L12,H8这五个特殊的点;
D,棋子分黑白两色,旗子上显示步数.
参考代码如下
private void drawBoard(Canvas canvas) { paint.setColor(Color.BLACK); paint.setStrokeWidth(dip2px(0.7f)); textPaint.setTextSize(dip2px(12)); textPaint.setColor(Color.BLACK); int _width = getWidth(); int _height = getHeight(); int _chessboardWidth = _width < _height ? _width : _height; float _padding = _chessboardWidth * 0.2f; float _lineLength = _chessboardWidth * 0.8f; float _space = _lineLength / 14; float _startX = _padding / 2; float _startY = _padding / 2; //画棋盘 //1,画竖线 for (int i = 0; i < 15; i++) { canvas.drawLine(_startX, _startY, _startX, _startY + _lineLength, paint); _startX += _space; } //2,画横线 _startX = _padding / 2; _startY = _padding / 2; for (int i = 0; i < 15; i++) { canvas.drawLine(_startX, _startY, _startX + _lineLength, _startY, paint); _startY += _space; } //3,画数字字母 _startX = _padding / 2; _startY = _padding / 8 * 3; char _text = 'A'; for (char i = 0; i < 15; i++) { String s = String.valueOf((char) (_text + i)); canvas.drawText(s, _startX - textPaint.measureText(s) / 2, _startY, textPaint); _startX += _space; } _startX = _padding / 4; _startY = _padding / 2; for (int i = 1; i <= 15; i++) { String s = String.valueOf(i); Rect rect = new Rect(); paint.getTextBounds(s, 0, s.length(), rect); canvas.drawText(s, _startX - rect.width(), _startY + rect.height(), textPaint); _startY += _space; } //4,画星,天元 canvas.drawCircle(_padding / 2 + _space * 3, _padding / 2 + _space * 3, _space / 10, paint); canvas.drawCircle(_padding / 2 + _space * 3, _padding / 2 + _space * 11, _space / 10, paint); canvas.drawCircle(_padding / 2 + _space * 11, _padding / 2 + _space * 3, _space / 10, paint); canvas.drawCircle(_padding / 2 + _space * 11, _padding / 2 + _space * 11, _space / 10, paint); canvas.drawCircle(_padding / 2 + _space * 7, _padding / 2 + _space * 7, _space / 10, paint); }
-
添加棋盘上的落点.棋盘上一共有15*15个能落子的点,每个点都有自己唯一的编号(例如F6,H3).记录每个点的编号和位置并放在Map中.
代码如下
private Map<String, ChessPoint> chessPoints; private void addPoints() { if (chessPoints.size() != 15 * 15) { int _width = getWidth(); int _height = getHeight(); int _chessboardWidth = _width < _height ? _width : _height; float _padding = _chessboardWidth * 0.2f; float _lineLength = _chessboardWidth * 0.8f; float _space = _lineLength / 14; float _startX = _padding / 2; float _startY = _padding / 2; chessboardPadding = _padding / 2; chessboardSpace = _space; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { ChessPoint chessPoint = new ChessPoint(); chessPoint.setPoint(new Point((int) (_startX + _space * i), (int) (_startY + _space * j))); chessPoint.setXName(String.valueOf((char) ('A' + i))); chessPoint.setYName(String.valueOf(1 + j)); chessPoint.setName(String.valueOf((char) ('A' + i)) + String.valueOf(1 + j)); chessPoints.put(chessPoint.name, chessPoint); } } } }
ChessPoint
代码如下public static class ChessPoint { //省略get,set方法 private String name; private String XName; private String YName; private Point point; @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj instanceof Point) { Point _point = (Point) obj; return (_point.x == point.x && _point.y == point.y); } if (obj instanceof ChessPoint) { ChessPoint _chessPoint = (ChessPoint) obj; return _chessPoint.point.x == this.point.x && _chessPoint.point.y == this.point.y; } return false; } }
绘制已存在的棋子
棋子分为两种,已经落下的棋子和正在下的棋子.正在下的棋子位置会随着手指的滑动尔移动,当手指离开棋盘的一刹那,该棋子的位置确定.
-
画已已经落下的棋子
//已经落下的白棋的集合 private Map<String, ChessMan> WhiteChessmen = new HashMap<>(); //已经落下的黑棋的集合 private Map<String, ChessMan> BlackChessmen = new HashMap<>(); private void drawChessMans(Canvas canvas) { int _width = getWidth(); int _height = getHeight(); int _chessboardWidth = _width < _height ? _width : _height; float _lineLength = _chessboardWidth * 0.8f; float _space = _lineLength / 14; float radius = _space * 0.45f; textPaint.setTextSize(radius); paint.setColor(0xFFDDDDDD); textPaint.setColor(Color.BLACK); //遍历集合并绘制棋子和里面的字 for (Map.Entry<String, ChessMan> entry : WhiteChessmen.entrySet()) { Point point = entry.getValue().getPoint().point; canvas.drawCircle(point.x, point.y, radius, paint); String s = String.valueOf(entry.getValue().getChessNum()); Rect rect = new Rect(); textPaint.getTextBounds(s, 0, s.length(), rect); canvas.drawText(String.valueOf(entry.getValue().getChessNum()), point.x - rect.width() / 2, point.y + rect.height() / 2, textPaint); } paint.setColor(Color.BLACK); textPaint.setColor(Color.WHITE); for (Map.Entry<String, ChessMan> entry : BlackChessmen.entrySet()) { Point point = entry.getValue().getPoint().point; canvas.drawCircle(point.x, point.y, radius, paint); String s = String.valueOf(entry.getValue().getChessNum()); Rect rect = new Rect(); textPaint.getTextBounds(s, 0, s.length(), rect); canvas.drawText(String.valueOf(entry.getValue().getChessNum()), point.x - rect.width() / 2, point.y + rect.height() / 2, textPaint); } }
-
画正在下的棋子.这里需要添加滑动事件的监听,好知道旗手落子的位置.
private boolean isGameOver = false; //用一组临时变量记录棋手触摸的位置 private float freeX = -1; private float freeY = -1; @Override public boolean onTouchEvent(MotionEvent motionEvent) { //如果游戏已经结束,则不监听该事件 if (isGameOver) { return super.onTouchEvent(motionEvent); } //正在触摸的位置(相对于这个view) float _x = motionEvent.getX(); float _y = motionEvent.getY(); Point point = new Point((int) _x, (int) (_y)); //判断正在触摸位置是不是在棋盘内部.不是我们就不管了 if (clickInBoard(point)) { switch (motionEvent.getAction()) { //记录位置,刷新画面 case MotionEvent.ACTION_DOWN: freeX = _x; freeY = _y; invalidate(); break; //记录位置,刷新画面 case MotionEvent.ACTION_MOVE: freeX = _x; freeY = _y; invalidate(); break; //抬起时,位置确定. case MotionEvent.ACTION_UP: freeX = -1; freeY = -1; determineLocation(point); invalidate(); break; } return true; } else { return super.onTouchEvent(motionEvent); } }
clickInBoard
方法具体如下private boolean clickInBoard(Point point) { return !(point.x < chessboardPadding) && !(point.x > chessboardPadding + chessboardSpace * 14) && !(point.y < chessboardPadding) && !(point.y > chessboardPadding + chessboardSpace * 14); }
有了
freeX
和freeY
两个临时变量,绘制该棋子的位置就很简单了.这里引入了一个新的变量isWhiteActive
用来判断正在下棋的是白方还是黑方//白方下还是黑方下 private boolean isWhiteActive = false; private void drawFreeChessMans(Canvas canvas) { if (freeX == -1 || freeY == -1) { return; } int _width = getWidth(); int _height = getHeight(); int _chessboardWidth = _width < _height ? _width : _height; float _lineLength = _chessboardWidth * 0.8f; float _space = _lineLength / 14; float radius = _space * 0.45f; if (isWhiteActive) { paint.setColor(Color.WHITE); } else { paint.setColor(Color.BLACK); } canvas.drawCircle(freeX, freeY, radius, paint); }
-
当一方的手抬起来时,判断落子的范围.如果抬起来的地方没有子,则视为落子.否则,啥也不用做.如果落子了,则判断有没有达到胜利条件.达到胜利条件,则游戏结束;否则,另一方开始下棋.
private void determineLocation(Point point) { float minDistance = Integer.MAX_VALUE; float _distance; ChessPoint chessPoint = null; //遍历棋盘上所有的点,找到离落子点最近距离的点. for (Map.Entry<String, ChessPoint> entry : chessPoints.entrySet()) { _distance = getDistance(point, entry.getValue().point); if (_distance <= minDistance) { minDistance = _distance; chessPoint = entry.getValue(); } } if (chessPoint != null) { //1.查看该位置上有没有子 //2.有子,无效.没子,加到里面去 if (checkHasPoint(chessPoint)) { //3.已经有该棋子了,这一步无效 } else { //4.没有该棋子,则把该棋子添加到对应的集合里面去 ChessMan chessMan = new ChessMan(); chessMan.setPoint(chessPoint); chessMan.setChessNum(BlackChessmen.size() + WhiteChessmen.size() + 1); chessMan.setWhiteColor(isWhiteActive); if (isWhiteActive) { WhiteChessmen.put(chessMan.getPoint().name, chessMan); } else { BlackChessmen.put(chessMan.getPoint().name, chessMan); } lastActivityPoint = chessMan.getPoint().name; //5.检查游戏是否结束 checkGameOver(chessMan); isWhiteActive = !isWhiteActive; } } }
判断胜利条件
最后一步,也是最纠结的一步.思路如下
-
找到最后落子的点,判断该点周围
上下
/左右
/左上_右下
/右上_左下
这四组方向上的子是否为同一种颜色切加起来数目≥5. -
只需要判断当前的颜色.黑方落完子白方不可能胜利
-
只需要判断当前的子的八个方向.其他地方不可能出现五子连珠的情况
private void checkGameOver(ChessMan chessMan) { //1.找到落子的集合 Map<String, ChessMan> chessManList; if (chessMan.isWhiteColor()) { chessManList = WhiteChessmen; } else { chessManList = BlackChessmen; } //2.判断上下两个方向上是否五子连珠 if (chessManList.size() >= 5) { //上下 char _x = chessMan.getPoint().XName.charAt(0); int _y = Integer.valueOf(chessMan.getPoint().YName); //上 int up_count = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf(_x) + (_y - i); ChessMan _chessman = chessManList.get(str); if (_chessman != null) { up_count++; } else { break; } } int down_count = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf(_x) + (_y + i); ChessMan _chessman = chessManList.get(str); if (_chessman != null) { down_count++; } else { break; } } if (down_count + up_count + 1 >= 5) { //赢了 isGameOver = true; if (isWhiteActive) { Toast.makeText(getContext(), "白棋赢了", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), "黑棋赢了", Toast.LENGTH_SHORT).show(); } return; } //3.判断左右两个方向上是否五子连珠 int right_count = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf((char) (_x + i)) + _y; ChessMan _chessman = chessManList.get(str); if (_chessman != null) { right_count++; } else { break; } } int left_count = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf((char) (_x - i)) + _y; ChessMan _chessman = chessManList.get(str); if (_chessman != null) { left_count++; } else { break; } } if (left_count + right_count + 1 >= 5) { //赢了 isGameOver = true; if (isWhiteActive) { Toast.makeText(getContext(), "白棋赢了", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), "黑棋赢了", Toast.LENGTH_SHORT).show(); } return; } //4.判断这两个方向上是否五子连珠 int right_up = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf((char) (_x + i)) + (_y - i); ChessMan _chessman = chessManList.get(str); if (_chessman != null) { right_up++; } else { break; } } int left_down = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf((char) (_x - i)) + (_y + i); ChessMan _chessman = chessManList.get(str); if (_chessman != null) { left_down++; } else { break; } } if (right_up + left_down + 1 >= 5) { //赢了 isGameOver = true; if (isWhiteActive) { Toast.makeText(getContext(), "白棋赢了", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), "黑棋赢了", Toast.LENGTH_SHORT).show(); } return; } //5.判断这两个方向上是否五子连珠 int left_up = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf((char) (_x - i)) + (_y - i); ChessMan _chessman = chessManList.get(str); if (_chessman != null) { left_up++; } else { break; } } int right_down = 0; for (int i = 1; i <= 4; i++) { String str = String.valueOf((char) (_x + i)) + (_y + i); ChessMan _chessman = chessManList.get(str); if (_chessman != null) { right_down++; } else { break; } } if (left_up + right_down + 1 >= 5) { //赢了 isGameOver = true; if (isWhiteActive) { Toast.makeText(getContext(), "白棋赢了", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), "黑棋赢了", Toast.LENGTH_SHORT).show(); } return; } //6.如果上述都没有五子连珠的情况,看看棋盘里还有没有位置 if (BlackChessmen.size() + WhiteChessmen.size() == 15 * 15) { Toast.makeText(getContext(), "和棋", Toast.LENGTH_SHORT).show(); isGameOver = true; } } }
其他补充
-
添加悔棋,重置棋盘功能
//重置棋盘 public void reset() { WhiteChessmen.clear(); BlackChessmen.clear(); isGameOver = false; isWhiteActive = false; lastActivityPoint = ""; invalidate(); } //悔棋. //在determineLocation方法中记录最近一步棋的位置,然后在集合中删除这一步棋,刷新,就OK了 //这一步有个明显bug. private String lastActivityPoint = ""; public void repent() { BlackChessmen.remove(lastActivityPoint); WhiteChessmen.remove(lastActivityPoint); isWhiteActive = !isWhiteActive; isGameOver = false; invalidate(); } //绑定点击事件 repentBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { chessboardView.repent(); } }); resetBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { chessboardView.reset(); } });
-
棋子对象代码
public class ChessMan { //省略了get,set方法 private ChessboardView.ChessPoint point; private boolean whiteColor; private int chessNum; }
-
拓展自定义属性
是否显示棋子上的数字,是否记录棋谱…这些都很好实现,就不一一赘述了
-
添加AI,实现人机五子棋
正在实现中…有空的话再来补充