美文网首页Android 自定义viewAndroid开发Android知识
Android进阶之自定义五子棋与其基本逻辑

Android进阶之自定义五子棋与其基本逻辑

作者: while1love | 来源:发表于2017-05-30 22:42 被阅读113次

1.概述


通过本文的这次学习,我们可以掌握五子棋棋盘的自定义实现以及基本下棋逻辑的实现、以及继续巩固自定义View所必备的方法的使用。

五子棋.gif

2.小复习


通过实现自定义TextView之后,我们基本上掌握了自定义View需要重写的几个方法,例如:
* 构造函数(Context context, @Nullable AttributeSet attrs)
* onMeasure(int widthMeasureSpec, int heightMeasureSpec)
* AT_MOST EXACTLY UNSPECIFIED
* onDraw(Canvas canvas)
* onTouchEvent(MotionEvent event)
用以上四个方法进行初始化变量、测量、绘制、交互操作。
再接下来的实现过程中我们仍需要实现这几个方法。
如果没有掌握或者不太熟练的同学,请翻看我的前一篇博文

3.该案例的实现分析


3.1:画行、列

先获取确定的画板大小、以及棋盘的行数因为棋盘大小确定了,行数列数确定了(在这个案例中行数与列数相一致)那么我们就可以画行画列了

3.2:画棋子

根据行数列数得到一个格子的高度值,再计算这个棋子所占的比例,然后绘制到画板上

3.3:添加交互事件

点击棋盘相应点的位置可以下棋,并且判断是否已经Game Over

4.实现过程及代码

大致过程:
自定义一个继承View的类,在该类中,获取你的自定义的属性(从attrs.xml以及你layout定义该控件时的属性),并且在该类中实现相应的测量,绘制,以及交互。
然后,通过一个算法判定是否结束游戏。
最后,就可以在任意一个layout.xml中添加你的控件了

4.1 创建attrs.xml 并且自定义属性

<pre>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="wuziqi_board">
<attr name="backgroundColor" format="Color"></attr>
<attr name="boardMaxLine" format="integer"></attr>
</declare-styleable>
</resources></pre>

4.2 创建WuZiQiBoard类
  • 4.2.1在构造方法中获取自定义属性值
    其次是获得棋子的图片以及初始化画笔的属性
    <pre> public WuZiQiBoard(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);

      TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.wuziqi_board);
    

// 获取背景颜色以及最大行数
// backGroundColor MAX_LINE 都是全局变量 int类型
backGroundColor = typedArray.getColor(R.styleable.wuziqi_board_backgroundColor, 0x44ff0000);
MAX_LINE = typedArray.getInt(R.styleable.wuziqi_board_boardMaxLine, 12);

    setBackgroundColor(backGroundColor);
    wightPiece = BitmapFactory.decodeResource(getResources(), R.drawable.stone_w2);
    blackPiece = BitmapFactory.decodeResource(getResources(), R.drawable.stone_b1);
    mPaint = new Paint();
    mPaint.setColor(Color.BLACK);
    mPaint.setDither(true);
    mPaint.setDither(true);
    mPaint.setStyle(Paint.Style.STROKE);
}

</pre>

  • 4.2.2测量View的大小并且开始绘制画板
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode((heightMeasureSpec));
//      如果是嵌套在ScrollView中,则需要判断widthMode是否为Exactly或heightMode是否为Exactly
        if (widthMode == MeasureSpec.UNSPECIFIED) {
//      让width由height决定
            widthSize = heightSize;
        } else if (heightMode == MeasureSpec.UNSPECIFIED){
            heightSize = widthSize;
        }
        int width = Math.min(widthSize, heightSize);
        setMeasuredDimension(width, width);
    }

这段代码的目的则是:
绘制该View大小,如果传进来的参数都为match_parent,那么该View所占宽高都为屏幕最宽的大小。
如果传进来的参数都为定值:如宽200dp,高100dp,那么该View的大小则都为100dp。
如果传进来的参数都为wrap_content,那么该View的大小都为

  • 4.2.3 重写onSizeChanged()
    该方法是当View的大小发生改变时就会调用。所以,我们设想一下,当View的大小发生改变,那些变量会因此而受影响。将她直接初始化以及计算的过程放在该方法中。
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
//      一旦View大小改变,那么哪些变量会因此而改变呢?
//      最初我们说的有行高、黑白棋子的大小
        mPanelWidth = w;        //棋盘宽度
        mLineHeight = mPanelWidth * 1.0F / MAX_LINE;    //一行的高度,也就是一个格子的宽度
//      再根据比例计算棋子的大小
        int pieceWidth = (int) (mLineHeight * RADIO_PIECE_OF_LINE_HEIGHT);
//      根据计算的棋子大小来缩放棋子的大小
//      参数 pieceWith  是我们期望这个棋子的大小
        whitePiece = Bitmap.createScaledBitmap(whitePiece, pieceWidth, pieceWidth, false);
        blackPiece = Bitmap.createScaledBitmap(blackPiece, pieceWidth, pieceWidth, false);
    }
  • 4.2.4 重中之中,开始进行绘制onDraw()
    在这个方法中我们要实现两个方法
    1.)绘制棋盘
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawBoard(canvas);    //绘制棋盘
        drawPieces(canvas);   //绘制棋子
    }
private void drawBoard(Canvas canvas) {

//      drawBoard()实际上就是一个画线的过程
//      画横线   由于棋子在最左上角绘制时 还需要占额外的空间
//               所以绘制棋盘时我们额外留半个格子的大小
//      注意      startX stopX startY stopY 都是坐标都是坐标都是坐标!!!
        for (int i = 0; i < MAX_LINE; i++) {
            int startX = (int) (mLineHeight / 2);
            int stopX = (int) (mPanelWidth - mLineHeight / 2);
            int y = (int) (mLineHeight / 2 + mLineHeight * i);
            canvas.drawLine(startX, y, stopX, y, mPaint);
        }
//        画竖线
        for (int i = 0; i < MAX_LINE; i++) {
            int startY = (int) (mLineHeight / 2);
            int stopY = (int) (mPanelWidth - mLineHeight / 2);
            int x = (int) (mLineHeight / 2 + mLineHeight * i);
            canvas.drawLine(x, startY, x, stopY, mPaint);
        }
    }

写完以上代码,测试效果如下,此时我们点击棋盘是没有任何反应的。

绘制棋盘效果图.png

现在我们开始准备绘制棋子。
2.)绘制棋子

棋子的大小我们事先已在onSizedChanged()中修改完毕,接下来我们就通过他们的位置绘制即可。但是,我们又没有点击,是不是又要先进行事件的响应?对了,先进行点击事件的响应,然后在通过刷新界面调用onDraw()对我们的棋子进行绘制。

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        Point point;
        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                
                point = getPoint(event.getX(), event.getY());

//               判断如果鼠标在View之外 则取消掉本次响应事件
//               注意前面有个感叹号!手离开屏幕时的点的位置如果在界面外就…
                if (!((event.getX() >= 0 && event.getX() <= mPanelWidth)
                        && (event.getY() >= 0 && event.getY() <= mPanelWidth))) {
                    return false;
                }

//               先判断是否已经GameOver 如果是的话则不响应本次点击事件

                if (isGameOver) {
                    return false;
                }

//               先进行判断 如果该棋子存在的话,将不响应该次点击事件
//                因为仅靠event.getX() getY()来判定是否是同一个点显然是不合理的
//                所以我们要引入Point 结合 数组的坐标值。来判定是否该点存在?
//                ----------------------------------------------------------
//                我们将以mLineHeight的一半为半径以内的点都看作是一个Point,只要在该范围内的点击点都将
//                初始化为该Point
//                ----------------------------------------------------------
                if (whitePieces.contains(point) || blackPieces.contains(point)) {
                    return false;
                }

//              判断应是白棋嘛  如果是的话添加到白棋队列
                if (isWhite) {
                    whitePieces.add(point);
                    panelArray[(int) (event.getX() / mLineHeight)][(int) (event.getY() / mLineHeight)] = IS_WHITE_PIECE;

                } else {
                    blackPieces.add(point);
                    panelArray[(int) (event.getX() / mLineHeight)][(int) (event.getY() / mLineHeight)] = IS_BLACK_PIECE;
                }

//              刷新当前View,将五子棋绘制到棋盘上
                invalidate();

//              此处应判断当棋子落在棋盘上是否会导致游戏GameOver

                break;
        return true;
    }
private Point getPoint(float x, float y) {
        Point point = new Point((int) (x / mLineHeight), (int) (y / mLineHeight));
        return point;
    }
   private void drawPieces(Canvas canvas) {

        for (Point point : whitePieces) {
            canvas.drawBitmap(whitePiece, (point.x + (1 - RADIO_PIECE_OF_LINE_HEIGHT) / 2) *
                    mLineHeight, (point.y + (1 - RADIO_PIECE_OF_LINE_HEIGHT) / 2) *
                    mLineHeight, null);
        }
        for (Point point : blackPieces) {
            canvas.drawBitmap(blackPiece, (point.x + (1 - RADIO_PIECE_OF_LINE_HEIGHT) / 2) *
                    mLineHeight, (point.y + (1 - RADIO_PIECE_OF_LINE_HEIGHT) / 2) *
                    mLineHeight, null);
        }

    }

你会发现,此时五子棋的点击、绘制已经告一段落了。接下来就是进行判断落子后是否结束当前游戏,也就是说其中一方快乐的打出GG的控制代码。
我们需要一个和棋盘相同大小的数组,并且在点击该点(成功放进whitePieces或者blackPieces队列)的时候标记该数组,这样我们进行判定的时候会方便许多。

在onTouchEvent 中 case MotionEvent.ACTION_UP:最后一段注释后面添加:


                isGameOver = checkIsGameOver(point);
                String text = isWhite ? "白棋获胜" : "黑棋获胜";
                if (isGameOver)
                    Toast.makeText(getContext(), text, Toast.LENGTH_SHORT).show();
                isWhite = !isWhite;
                break;
private boolean checkIsGameOver(Point point) {
        //                  ↖↑↗
        //                  ←十→   从这八个方向判定是否连成了五子
        //                  ↙↓↘
//      八个方向
//      定一个点在中间  向上移是
        int top[] = {-1, 0};
        int bottom[] = {1, 0};
        int left[] = {0, -1};
        int right[] = {0, 1};
        int top_right[] = {-1, 1};
        int top_left[] = {-1, -1};
        int bottom_right[] = {1, 1};
        int bottom_left[] = {1, -1};

        boolean check1 = checkTopAndBottom(point, panelArray, top, bottom);
        if (check1)
            return true;

        boolean check2 = checkTopAndBottom(point, panelArray, right, left);

        if (check2)
            return true;
        boolean check3 = checkTopAndBottom(point, panelArray, top_right, bottom_left);
        if (check3)
            return true;
        boolean check4 = checkTopAndBottom(point, panelArray, top_left, bottom_right);
        if (check4)
            return true;

        return false;
    }
private boolean checkTopAndBottom(Point point, int[][] panelArray, int[] check1, int[] check2) {

        //从这个点 先遍历其上层的位置的点
        //再从这个点 遍历下层的位置
        //并且返回 boolean值
        int topCount = 0;
        int bottomCount = 0;
        int x = point.x;
        int y = point.y;
        int type = panelArray[x][y];
        while ((x + check1[0] > 0 && x + check1[0] < MAX_LINE) && (y + check1[1] > 0 && y + check1[1] < MAX_LINE)) {
            x += check1[0];
            y += check1[1];
            if (type == panelArray[x][y]) {
                topCount++;
            } else {
                break;
            }
            if (topCount == 4) {
                return true;
            }
        }
        int x_bottom = point.x;
        int y_bottom = point.y;
        while ((x_bottom + check2[0] > 0 && x_bottom + check2[0] < MAX_LINE) && (y_bottom + check2[1] > 0 && y_bottom + check2[1] < MAX_LINE)) {
            x_bottom += check2[0];
            y_bottom += check2[1];
            if (type == panelArray[x_bottom][y_bottom]) {
                bottomCount++;
            } else {
                break;
            }
            if (bottomCount == 4) {
                return true;
            }
        }
        if (topCount + bottomCount + 1 >= 5)
            return true;
        return false;


    }
4.3 接下来就是在你的布局文件中引入该控件即可
Paste_Image.png

这里就不再多做解释了,注意xmlns:app 即可

5.总结

final_wuziqi.png

最终的实现效果如上,是不是感觉有些出入?
别怕,还有剩下的两个细节留给你们自己完成:

  • 1.自己在drawable下新建个custom_background.xml然后定义corners标签的radius的值即可,然后在包裹该棋盘的LinearLayout background 定义为你刚写的drawable。
  • 2.在layout根布局中添加background,添加背景图片。在包裹五子棋控件的布局中设置内边距为10dp即可达到预期的效果。

总结下自定义View的实现步骤
1.自定义属性
2.创建继承View的类 初始化属性,测量,绘制,完成交互代码
3.在布局文件中引用使用。
其实这么一总结,真的没有什么,翻来覆去就是个这。


1.通过这两篇文章,相信你已经掌握了自定义View的要点了。如果还未掌握,还想继续啃,那么推荐你去看HongYang的博客。
2.下篇自定义View不会有这么详细了。
从GitHub下载源码

相关文章

网友评论

    本文标题:Android进阶之自定义五子棋与其基本逻辑

    本文链接:https://www.haomeiwen.com/subject/omlrfxtx.html