Android 自定义 View --- 实现五子棋游戏

直接上效果图

Android 自定义 View --- 实现五子棋游戏_第1张图片

原理

从棋盘到棋子,到开始下棋的各类点击事件,均在 ChessView 中实现,这个 View 没有提供自定义属性(因为我觉得没有必要~~~)。

项目GitHub地址:https://github.com/hunlianyun/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 包里面的对话框,因为这样可以在版本较低的安卓平台下也可以得到不错的显示效果,效果如下:
    Android 自定义 View --- 实现五子棋游戏_第2张图片
/**
     * 游戏结束,显示对话框
     */
    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 布局文件如下,非常简单,我相信不用多说都能看懂:


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

你可能感兴趣的:(Android)