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下载源码









网友评论