最近一直在学习Android自定义View方面的知识,正好看到一个讲解制作五子棋小游戏的案例,遂学习一番,记录下学习过程,帮助那些有需要的人。

首先放上效果图:

下面我将带领大家一步步完成这个五子棋小游戏。

一、创建自定义View类及定义成员变量

首先我们先定义一个类WuziqiPanel,让该类继承自View,并在类中定义一些成员变量,便于我们后面使用,而且在我们需要显示五子棋的布局文件中引入该自定义View。

WuziqiPanel.java文件

public class WuziqiPanel extends View{//棋盘宽度private int mPanelWidth;//棋盘格子的行高(声明为int会造成由于不能整除而造成的误差较大)private float mLineHeight;//棋盘最大行列数(其实就是棋盘横竖线的个数)private int MAX_LINE_NUM = 10;//定义画笔绘制棋盘格子private Paint mPaint = new Paint();//定义黑白棋子Bitmapprivate Bitmap mWhitePiece;private Bitmap mBlackPiece;//棋子的缩放比例(行高的3/4)private float pieceScaleRatio = 3 * 1.0f / 4;//存储黑白棋子的坐标private ArrayList<Point> mWhiteArray = new ArrayList<>();private ArrayList<Point> mBlackArray = new ArrayList<>();//哪方先下子private boolean isWhiteFirst = true;//游戏是否结束private boolean isGameOver;//确定赢家private boolean isWhiteWinner = false;//游戏结束监听private OnGameOverListener onGameOverListener;
}

activity_main.xml文件中引入该自定义View

activity_main.xml文件

<?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:id="@+id/activity_main"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/main_bg"tools:context="com.codekong.wuziqi.activity.MainActivity"><com.codekong.wuziqi.view.WuziqiPanelandroid:id="@+id/id_wuziqi_panel"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_centerInParent="true" />
</RelativeLayout>

注意:自定义View的引入必须使用完整路径

二、定义构造函数并初始化设置

这一步我们首先要书写构造函数,并且在构造函数中初始化一些设置。比如初始化画笔以及将棋子图片转为bitmap
这里我们在三个参数的构造方法中调用两个参数的构造方法,又在两个参数的构造方法中调用一个参数的构造方法。

这里简单解释一下。一个参数的构造方法是我们在new出一个组件的时候调用;两个参数的构造方法是我们在XML中使用自定义View时调用;三个参数的构造方法是我们自定义View中使用了自定义属性的时候调用;所以我们按上面的写法就可以覆盖到这三种情况。

public WuziqiPanel(Context context) {this(context, null);
}public WuziqiPanel(Context context, AttributeSet attrs) {this(context, attrs, 0);
}public WuziqiPanel(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();
}/*** 初始化设置*/
private void init() {//初始化画笔mPaint.setColor(0x88000000);//设置抗锯齿mPaint.setAntiAlias(true);//设置防抖动mPaint.setDither(true);//设置为空心(画线)mPaint.setStyle(Paint.Style.STROKE);//初始化棋子mWhitePiece = BitmapFactory.decodeResource(getResources(), R.drawable.icon_white_piece);mBlackPiece = BitmapFactory.decodeResource(getResources(), R.drawable.icon_black_piece);
}

三、测量

测量几乎是自定义View必须要经历的步骤,由于我们要先绘制棋盘,所以我们必须先测量出我们需要的数据。
我们在onMeasure()方法中拿到屏幕宽高,然后在onSizeChanged()中获得棋盘的宽度,计算出棋盘的行高。接着根据行高缩放棋子大小,使其显示大小合适。

/*** 测量* @param widthMeasureSpec* @param heightMeasureSpec*/
@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);int width = Math.min(widthSize, heightSize);//此处的逻辑判断是处理当我们自定义的View被嵌套在ScrollView中时,获得的测量模式// 会是UNSPECIFIED// 使得到的widthSize或者heightSize为0if (widthMode == MeasureSpec.UNSPECIFIED){width = heightSize;}else if (heightMode == MeasureSpec.UNSPECIFIED){width = widthSize;}//调用此方法使我们的测量结果生效setMeasuredDimension(width, width);
}
/*** 当宽高发生变化时回调此方法* @param w* @param h* @param oldw* @param oldh*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);//此处的参数w就是在onMeasure()方法中设置的自定义View的大小//计算出棋盘宽度和行高mPanelWidth = w;mLineHeight = mPanelWidth * 1.0f / MAX_LINE_NUM;//将棋子根据行高变化int pieceWidth = (int) (pieceScaleRatio * mLineHeight);mWhitePiece = Bitmap.createScaledBitmap(mWhitePiece, pieceWidth, pieceWidth, false);mBlackPiece = Bitmap.createScaledBitmap(mBlackPiece, pieceWidth, pieceWidth, false);
}

注意:如上面注释所写,由于我们不知道我们的自定义View将会被放在什么样的布局中,所以如果我们的五子棋盘被放在ScrollView中,我们测量到的宽或者高就会有一方为0,就会使我们的测量失效,从而影响后面的绘制,所以我们必须处理这一种情况.

四、绘制棋盘

首先我们应该先绘制好我们要下棋的棋盘,我们此处准备横竖都画10条线来绘制我们的棋盘,其实此处的棋盘的横竖线的个数我们是可以修改的,棋子的大小也会随着棋盘格子的大小缩放,但是为了美观一些,我们此处采用横竖10条线.

大家可以先通过我下面的示意图来理解一下棋盘横竖线坐标的确定.

/*** 绘制棋盘* @param canvas*/
private void drawBoard(Canvas canvas) {int w = mPanelWidth;float lineHeight = mLineHeight;for (int i = 0; i < MAX_LINE_NUM; i++) {int startX = (int) (lineHeight / 2);int endX = (int) (w - lineHeight / 2);int y = (int) ((0.5 + i) * lineHeight);//画横线canvas.drawLine(startX, y, endX, y, mPaint);//画竖线canvas.drawLine(y, startX, y, endX, mPaint);}
}

通过上面的步骤棋盘就算是绘制好了.

五、处理用户手势-下棋

上面我们绘制好了棋盘,接着我们就可以下棋啦,所以理所当然我们要开始处理用户的手势,开始下棋啦,所以我们要重写onTouchEvent().

这一步我们要做三件事:

1 . onTouchEvent()return true拦截手势事件我们自己处理

2 . 获得用户触摸的坐标并进行处理,处理为棋盘上的整数值坐标,并存储起来.

这一步我们调用一个自定义的函数getValidPoint()将用户点击的点的坐标转化为整数值.也就是说用户下子的时候不必精确点击到棋盘各自的交叉点上,而是只要在这个交叉点周围就可以了,我们只需要将其取整后除以我们格子的高度(行高),

简单解释一下,如下图1、2所指的箭头所示,由于我们的棋盘上下左右边距为0.5倍的行高,当我们手指在以棋盘顶点0.5倍的行高范围内点击,只要除以行高取整,就会得到该顶点坐标.比如我点击的坐标点为(0.75,0.82),在第一个圈范围内,此时行高为,除以1后取整得到(0,0)就是棋盘的顶点,就是我们需要落子的地方。

/*** 将用户点击的位置的Point转换为类似于(0,0)d的坐标* @param x* @param y* @return*/
private Point getValidPoint(int x, int y) {return new Point((int) (x / mLineHeight), (int) (y / mLineHeight));
}

3 . 调用invalidate()方法进行界面重绘,绘制出用户所下的棋子

每次调用invalidate()方法就会调用onDraw()方法进行界面绘制,在该方法中先绘制棋盘,然后绘制用户所下的棋子,还要判断游戏结束.绘制棋子和判断游戏结束我会在后面的步骤中给出介绍.

4 . 将下棋权利交于另一方(白棋下完换黑棋)

这一步比较好处理,我们只需要将一个布尔变量isWhiteFirst取反,下一次就轮到另一种棋子下子了.

/*** 处理用户手势操作* @param event* @return*/
@Override
public boolean onTouchEvent(MotionEvent event) {if (isGameOver) return false;int action = event.getAction();//手指抬起后处理if (action == MotionEvent.ACTION_UP){//拦截事件自己来处理int x = (int) event.getX();int y = (int) event.getY();Point point = getValidPoint(x, y);//首先判断所点击的位置是不是已经有棋子if (mWhiteArray.contains(point) || mBlackArray.contains(point)){return false;}//白棋先下if (isWhiteFirst){mWhiteArray.add(point);}else{mBlackArray.add(point);}//调用重绘invalidate();isWhiteFirst = !isWhiteFirst;}return true;
}
/*** 进行绘制工作* @param canvas*/
@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);//绘制棋盘drawBoard(canvas);//绘制用户已经下的所有棋子drawPieces(canvas);//判断游戏是否结束checkGameOver();
}

六、绘制棋子

上一步我们已经把用户所点击的要下棋子的坐标存储在了ArrayList,这一步我们就将遍历这个ArrayList将黑白棋子绘制到棋盘上.

这里我们的一个变量pieceScaleRatio = 3/4,表示一个棋子长宽为3/4的行高,剩余的1/4的行高平均棋子的左右各留出1/8的行高,这样棋子距离左右边框的距离为1/8行高,棋子与棋子之间的间距为2*1/8=1/4行高.

/*** 绘制棋子* @param canvas*/
private void drawPieces(Canvas canvas) {//绘制白棋子for (int i = 0, n = mWhiteArray.size(); i < n; i++) {Point whitePoint = mWhiteArray.get(i);//棋子之间的间隔为1/4行高canvas.drawBitmap(mWhitePiece,(whitePoint.x + (1 - pieceScaleRatio) / 2) * mLineHeight,(whitePoint.y + (1 - pieceScaleRatio) / 2) * mLineHeight, null);}//绘制黑棋子for (int i = 0, n = mBlackArray.size(); i < n; i++) {Point blackPoint = mBlackArray.get(i);//棋子之间的间隔为1/4行高,棋子距离左右边框的距离为1/8行高canvas.drawBitmap(mBlackPiece,(blackPoint.x + (1 - pieceScaleRatio) / 2) * mLineHeight,(blackPoint.y + (1 - pieceScaleRatio) / 2) * mLineHeight, null);}
}

七、判断游戏结束

经过上面的步骤,我们已经可以在棋盘上落子了,下面的任务就是我们要判断游戏是否结束.
这个游戏的规则比较简单,我们只要判断在上下左右斜对角线如果存在连续5个棋子是同一色,我们就可以判定胜负了.

我们专门定义一个工具类WuziqiUtil.java来进行判断

public class WuziqiUtil {//每行上最大的数目public static final int MAX_COUNT_IN_LINE = 5;/*** 检查是否五子连珠* @param points* @return*/public static boolean checkFiveInLine(List<Point> points) {for (Point p: points) {int x = p.x;int y = p.y;boolean win = checkHorizontal(x, y, points);if (win) return true;win = checkVertical(x, y, points);if (win) return true;win = checkLeftDiagonal(x, y, points);if (win) return true;win = checkRightDiagonal(x, y, points);if (win) return true;}return false;}/*** 判断x, y位置的棋子是否横向五个一致* @param x* @param y* @param points* @return*/public static boolean checkHorizontal(int x, int y, List<Point> points) {int count = 1;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x - i, y))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x + i, y))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;return false;}/*** 判断x, y位置的棋子是否竖向五个一致* @param x* @param y* @param points* @return*/public static boolean checkVertical(int x, int y, List<Point> points) {int count = 1;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x, y - i))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x, y + i))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;return false;}/*** 判断x, y位置的棋子是否左斜向上五个一致* @param x* @param y* @param points* @return*/public static boolean checkLeftDiagonal(int x, int y, List<Point> points) {int count = 1;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x - i, y + i))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x + i, y - i))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;return false;}/*** 判断x, y位置的棋子是否右斜向下五个一致* @param x* @param y* @param points* @return*/public static boolean checkRightDiagonal(int x, int y, List<Point> points) {int count = 1;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x - i, y - i))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;for (int i = 1; i < MAX_COUNT_IN_LINE; i++) {if (points.contains(new Point(x + i, y + i))){count ++;}else {break;}}if (count == MAX_COUNT_IN_LINE) return true;return false;}
}

然后我们只要在在自定义View类中的checkGameOver()方法中进行调用就可以判断游戏是否结束

/*** 检查游戏是否结束*/
private void checkGameOver() {//检查是否五子连珠boolean whiteWin = WuziqiUtil.checkFiveInLine(mWhiteArray);boolean blackWin = WuziqiUtil.checkFiveInLine(mBlackArray);if (whiteWin || blackWin){isGameOver = true;isWhiteWinner = whiteWin;//String msg = isWhiteWinner ? "白子获胜" : "黑子获胜";//Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show();onGameOverListener.gameOver(isWhiteWinner);}
}

或许已经有人发现了,我上面还声明了一个游戏结束的监听,对的,我们作为一个自定义View当然要将游戏的胜负结果通过回调函数返回给使用者,让其自行处理.

/*** 游戏结束回调监听*/
public interface OnGameOverListener{void gameOver(boolean isWhiterWinner);
}
/*** 设置游戏结束回调监听* @param onGameOverListener*/
public void setOnGameOverListener(OnGameOverListener onGameOverListener){this.onGameOverListener = onGameOverListener;
}

到这里看起来我们的五子棋小游戏已经开发完成了,但是一个负责的程序员怎么可能满足于此呢,我们还要让我们的游戏更健壮.

八、防止游戏被回收

我们想象一个场景,当我们正在玩五子棋小游戏,正到关键时刻,来电话了,这时候我们去接电话,这时候我们的小游戏就相当于处于后台,假如这时候手机内存不足,那我们在后台的小游戏就可能被内存回收了,当我们打完电话,发现棋盘上一个棋子都没啦,是不是很伤心,为了解决这个问题,我们就需要重写onSaveInstanceState()方法和onRestoreInstanceState()方法来保存和恢复我们的游戏状态.

我们可以通过旋转屏幕模拟出上面提到的情况,旋转屏幕就会触发上面两个函数.

/*** 防止内存不足活动被回收*/
private static final String INSTANCE = "instance";
private static final String INSTANCE_GAME_OVER = "instance_game_over";
private static final String INSTANCE_WHITE_ARRAY = "instance_white_array";
private static final String INSTANCE_BLACK_ARRAY = "instance_black_array";@Override
protected Parcelable onSaveInstanceState() {Bundle bundle = new Bundle();bundle.putParcelable(INSTANCE, super.onSaveInstanceState());bundle.putBoolean(INSTANCE_GAME_OVER, isGameOver);bundle.putParcelableArrayList(INSTANCE_WHITE_ARRAY, mWhiteArray);bundle.putParcelableArrayList(INSTANCE_BLACK_ARRAY, mBlackArray);return bundle;
}@Override
protected void onRestoreInstanceState(Parcelable state) {if (state instanceof Bundle){Bundle bundle = (Bundle) state;isGameOver = bundle.getBoolean(INSTANCE_GAME_OVER);mWhiteArray = bundle.getParcelableArrayList(INSTANCE_WHITE_ARRAY);mBlackArray = bundle.getParcelableArrayList(INSTANCE_BLACK_ARRAY);super.onRestoreInstanceState(bundle.getParcelable(INSTANCE));return;}super.onRestoreInstanceState(state);
}

好了,这样我们的游戏就健壮了不少.

九、再来一局

一个游戏怎么可以只玩一次呢,所以我们这里还需要向使用者保留一个再来一局的方法.

/*** 重新开始,再来一局*/
public void restart(){mBlackArray.clear();mWhiteArray.clear();isGameOver = false;isWhiteWinner = false;//重绘invalidate();
}

十、使用该自定义View

定义已经全部完成了,现在使用就非常简单了.

activity_main.xml文件

<?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:id="@+id/activity_main"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/main_bg"tools:context="com.codekong.wuziqi.activity.MainActivity"><com.codekong.wuziqi.view.WuziqiPanelandroid:id="@+id/id_wuziqi_panel"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_centerInParent="true" />
</RelativeLayout>

MainActivity.java文件

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);WuziqiPanel panel = (WuziqiPanel) findViewById(R.id.id_wuziqi_panel);panel.setOnGameOverListener(new WuziqiPanel.OnGameOverListener() {@Overridepublic void gameOver(boolean isWhiterWinner) {//处理胜负结果}});}
}

十一、结语

游戏的介绍就到此为止了,希望可以帮助到需要的人.
源代码已经在Github开源,开源地址: https://github.com/codekongs/WuZiQi
欢迎大家start和fork
下集预告:五子棋小游戏之AI篇,通过算法实现人机对战。

Android五子棋小游戏之UI篇相关推荐

  1. python编程小游戏代码-Python小游戏之300行代码实现俄罗斯方块

    前言 本文代码基于 python3.6 和 pygame1.9.4. 俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块.但是想到旋转,停靠,消除等操作,感觉好像很 ...

  2. python小游戏源码-Python小游戏之300行代码实现俄罗斯方块

    Python小游戏之300行代码实现俄罗斯方块 来源:中文源码网 浏览: 次 日期:2019年11月5日 [下载文档: Python小游戏之300行代码实现俄罗斯方块.txt ] (友情提示:右键点上 ...

  3. python编写小游戏代码_Python小游戏之300行代码实现俄罗斯方块

    Python小游戏之300行代码实现俄罗斯方块 来源:中文源码网 浏览: 次 日期:2019年11月5日 [下载文档: Python小游戏之300行代码实现俄罗斯方块.txt ] (友情提示:右键点上 ...

  4. python小游戏代码大全-Python小游戏之300行代码实现俄罗斯方块

    前言 本文代码基于 python3.6 和 pygame1.9.4. 俄罗斯方块是儿时最经典的游戏之一,刚开始接触 pygame 的时候就想写一个俄罗斯方块.但是想到旋转,停靠,消除等操作,感觉好像很 ...

  5. python小游戏之二

    20行python代码的入门级小游戏 20行python代码的入门级小游戏_linmo8256的博客-CSDN博客_py游戏代码 猜数字小游戏python实现(可用来撩妹撩汉) 猜数字小游戏pytho ...

  6. Unity2D 小游戏之 RocketMouse

    这个小游戏源自这里.这几天闲时捡了点 Unity(很久没有摸它了),顺手将这个小游戏移植到了 Unity5.5.0,除了 Parallax Scrolling 还有点小问题外,其它功能全部完整.部分代 ...

  7. 安卓小游戏之2048

    前一段时间开始学习安卓开发时,写了一个小游戏 2048 因为顺便学习了一下界面跳转,所以在主界面即游戏界面前还有一个界面作为欢迎界面. 主界面代码如下: import android.app.Acti ...

  8. 初学安卓:安卓小游戏之2048

    前一段时间开始学习安卓开发时,写了一个小游戏 2048 因为顺便学习了一下界面跳转,所以在主界面即游戏界面前还有一个界面作为欢迎界面. 主界面代码如下: import android.app.Acti ...

  9. gamebuino制作的小游戏之2048代码分析 loop部分

    if(gb.update()) {DrawBoard();for( int x = 0; x < 16; x++ ) {Board2048Old[x] = Board2048[x];} gb.u ...

最新文章

  1. redis 系列25 哨兵Sentinel (高可用演示 下)
  2. spring初始化web_了解Spring Web初始化
  3. 启动项目的时候,一直打印日志
  4. 《慕课React入门》总结
  5. web渗透漏洞实例讲解视频课程
  6. IOS -- UICollectionView里面的cell点击,点击一个cell改变其他cell的状态
  7. 测试C语言malloc申请内存不释放结果
  8. 机器学习之 sklearn.preprocessing 模块
  9. Inspeckage,安卓动态分析工具
  10. pod install速度慢解决方案
  11. 【Vue】—异步组件
  12. 剑指offer(C++)-JZ6:从尾到头打印链表(数据结构-链表)
  13. C#基础11.1:static关键字
  14. 深入剖析Android音频(四)AudioTrack
  15. 260.只出现一次的数字III
  16. MACm1上lark无法使用共享屏幕问题
  17. 1210_MISRA_C规范学习笔记_指针使用的规范性
  18. 微信小程序点击事件传递自定义参数的方法和跨页面传递数据
  19. 思科交换机配置【串口初始配置】
  20. 小米手机qq邮箱收件服务器,小米手机无法添加邮箱?是你不会设置而已

热门文章

  1. 和IDEA一样好用的go语言IDE:Goland
  2. arduino使用oled代码_Arduino提高篇14—摇杆按键操作OLED
  3. 超级淘的安全性,科普一下超级淘到底有多安全
  4. 《音视频直播------总体概述》
  5. 【Web 技术】1207- 广告是如何通过 cookie 跟踪我们的?
  6. Python中使用Tkinter 快速布局编写桌面GUI程序
  7. 九齐单片机NY8B062D开发笔记(一)准备
  8. QQ魔法表情实现原理源代码下载
  9. 国际标准之国家编码 - ISO 3166
  10. Boost库网络编程