Android自定义View实现五子棋

首先看下实际效果

Android自定义View实现五子棋

实现的思路

  1. 绘制棋盘

  2. 绘制已经存在的棋子

  3. 监听触摸事件,判断落子

  4. 判断胜利条件

    重写onDraw方法如下

     @Override
     protected void onDraw(Canvas canvas) {
     	//绘制棋盘
     	drawBoard(canvas);
     	//添加棋盘上的点
     	addPoints();
    
     	if (!isGameOver) {
     		//绘制随手指滑动的棋子
         	drawFreeChessMans(canvas);
     	}
     	//绘制棋子
     	drawChessMans(canvas);
     }
    

实现的步骤

绘制棋盘

  1. 创建构造方法,初始化部分变量

     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);
     }
    
  2. 由于五子棋盘是正方形的,所以我们重写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);
     }
    
  3. 绘制棋盘(这里我找了百度百科五子棋下面的图片作为参考).可以看到:

    A,棋盘是横竖各有15条线;

    B,棋盘左边右边有数字/字母的标志;

    C,棋盘有D4,L4,D12,L12,H8这五个特殊的点;

    D,棋子分黑白两色,旗子上显示步数.

    Android自定义View实现五子棋

    参考代码如下

     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);
     }
    
  4. 添加棋盘上的落点.棋盘上一共有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;
         }
     }
    

绘制已存在的棋子

棋子分为两种,已经落下的棋子和正在下的棋子.正在下的棋子位置会随着手指的滑动尔移动,当手指离开棋盘的一刹那,该棋子的位置确定.

  1. 画已已经落下的棋子

     //已经落下的白棋的集合
     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);
         }
    
     }
    
  2. 画正在下的棋子.这里需要添加滑动事件的监听,好知道旗手落子的位置.

     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);
     }
    

    有了freeXfreeY两个临时变量,绘制该棋子的位置就很简单了.这里引入了一个新的变量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);
     }
    
  3. 当一方的手抬起来时,判断落子的范围.如果抬起来的地方没有子,则视为落子.否则,啥也不用做.如果落子了,则判断有没有达到胜利条件.达到胜利条件,则游戏结束;否则,另一方开始下棋.

     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;
             }
         }
     }
    

判断胜利条件

最后一步,也是最纠结的一步.思路如下

  1. 找到最后落子的点,判断该点周围上下/左右/左上_右下/右上_左下这四组方向上的子是否为同一种颜色切加起来数目≥5.

  2. 只需要判断当前的颜色.黑方落完子白方不可能胜利

  3. 只需要判断当前的子的八个方向.其他地方不可能出现五子连珠的情况

     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;
             }
    
         }
     }
    

其他补充

  1. 添加悔棋,重置棋盘功能

     //重置棋盘
     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();
         }
     });
    
  2. 棋子对象代码

     public class ChessMan {
     	//省略了get,set方法
         private ChessboardView.ChessPoint point;
         private boolean whiteColor;
         private int chessNum;
     
     }
    
  3. 拓展自定义属性

    是否显示棋子上的数字,是否记录棋谱…这些都很好实现,就不一一赘述了

  4. 添加AI,实现人机五子棋

    正在实现中…有空的话再来补充