利用自定义View实现扫雷游戏
游戏规则:
简单版的扫雷事实上就是一个9×9的矩阵,其中有十个点是雷,非雷方块的数字代表该方块周围八个方块中雷的个数。通过长按某一方块(方块会变红)认定该方块为玩家认为的雷,通过短按某一方块来“展开”该方块。
展开:如果该方块为雷,则游戏失败;如果该方块下为非零数字,则将该方块的数字告诉玩家;如果该方块下的数字为零,则展开该方块周围区域,直到展开到数字或者雷为止。
实现难点:
- 如何生成不重复的十个雷
- 如何生成非雷区域的数字
- 如何实现“展开”
基本思路:
首先定义两个9×9的矩阵,其中一个矩阵用来存放各个方块下的数字(-1代表雷),另一个用来存放该方块的颜色(0代表灰色,即默认色;1代表白色,即普通展开;2代表红色,即认定的雷)。
通过自定义View来实现。并且该自定义View的宽高设置为固定的901px(小方格的边长为100px,线的宽度为1px)。
每点击一次方块都会调用view的invalidate方法,进而会触发onDraw方法。在点击事件中更改颜色矩阵的值,并在onDraw方法中根据两个矩阵的值进行重绘。
代码展示:
- 布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.lanxingren.minesweeping.MineSweepingView
android:layout_width= "901px"
android:layout_height="901px"
android:layout_centerInParent="true"/>
</RelativeLayout>
- 主活动
package com.example.lanxingren.minesweeping; import android.support.v7.app.AppCompatActivity;
import android.os.Bundle; public class MainActivity extends AppCompatActivity { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- 自定义View
package com.example.lanxingren.minesweeping; import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast; import java.util.Random; public class MineSweepingView extends View { // private MineSweepingView (Context context) {
// super(context);
// } Context myContext; // 触摸方块左上角的点
Point leftTop; // 默认背景画笔
Paint strokePaint = new Paint(); // 涂色画笔,红色代表玩家认为的雷,白色代表展开该方块
Paint whitePaint = new Paint();
Paint redPaint = new Paint(); //绘制数字的画笔
Paint textPaint = new Paint(); // 代表每个坐标的颜色,其中0代表银灰色,1代表白色,2代表红色
int[][] colors;
// 代表每个坐标的数字,其中-1代表雷
int[][] numbers; //小格子边长
final int width = 100; //一行格子数
final int rowCount = 9; //雷的个数
final int mineCount = 10; //手势操作监听器
private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
//防止其他事件不执行,所以返回true
@Override
public boolean onDown(MotionEvent e) {
return true;
} @Override
public void onShowPress(MotionEvent e) { } //短按事件
@Override
public boolean onSingleTapUp(MotionEvent e) {
leftTop = findLeftTop(e.getX(), e.getY()); if (numbers[leftTop.x][leftTop.y] == -1) {
Toast.makeText(myContext, "你输了!", Toast.LENGTH_SHORT).show();
reset();
MineSweepingView.this.invalidate();
} else {
expand(leftTop.x, leftTop.y);
MineSweepingView.this.invalidate();
} return true;
} //根据扫雷逻辑展开小方块
private void expand(int x, int y) {
//如果是雷
if (numbers[x][y] == -1) {
return;
} else if (numbers[x][y] == 0 && colors[x][y] == 0) {
colors[x][y] = 1; //左上
if (x - 1 >= 0 && y - 1 >= 0) {
expand(x - 1, y - 1);
}
//上
if (y - 1 >= 0) {
expand(x, y - 1);
}
//右上
if (x + 1 < rowCount && y - 1 >= 0) {
expand(x + 1, y - 1);
}
//右
if (x + 1 < rowCount) {
expand(x + 1, y);
}
//右下
if (x + 1 < rowCount && y + 1 < rowCount) {
expand(x + 1, y + 1);
}
//下
if (y + 1 < rowCount) {
expand(x, y + 1);
}
//左下
if (x - 1 >= 0 && y + 1 < rowCount) {
expand(x - 1, y + 1);
}
//左
if (x - 1 >= 0) {
expand(x - 1, y);
}
} else {
colors[x][y] = 1;
} } @Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
} //长按事件
@Override
public void onLongPress(MotionEvent e) {
leftTop = findLeftTop(e.getX(), e.getY()); if (colors[leftTop.x][leftTop.y] != 1) {
colors[leftTop.x][leftTop.y] = 2;
MineSweepingView.this.invalidate();
}
} @Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}; private GestureDetector detector = new GestureDetector(onGestureListener); public MineSweepingView(Context context, AttributeSet attributeSet) {
super(context, attributeSet); myContext = context; strokePaint.setColor(Color.BLACK);
strokePaint.setStrokeWidth(1); whitePaint.setStyle(Paint.Style.FILL_AND_STROKE);
whitePaint.setColor(Color.WHITE); redPaint.setStyle(Paint.Style.FILL_AND_STROKE);
redPaint.setColor(Color.RED); textPaint.setColor(Color.BLACK);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(50);
Typeface typeface = Typeface.createFromAsset(context.getAssets(), "fonts/consola.ttf");
textPaint.setTypeface(typeface);
textPaint.setStyle(Paint.Style.FILL); reset();
} @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); canvas.drawColor(Color.GRAY); for (int i = 0; i <= canvas.getWidth(); i += width) {
canvas.drawLine(i, 0, i, canvas.getHeight(), strokePaint);
} for (int j = 0; j <= canvas.getHeight(); j += width) {
canvas.drawLine(0, j, canvas.getWidth(), j, strokePaint);
} Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float top = fontMetrics.top;
float bottom = fontMetrics.bottom; int grayCount = 0;
int redCount = 0; for (int x = 0; x < rowCount; x++) {
for (int y = 0; y < rowCount; y++) {
switch (colors[x][y]) {
//宽高各缩减一单位是为了防止把细线也给覆盖了
case 1://白色
canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, whitePaint);
if (numbers[x][y] != -1 && numbers[x][y] != 0) {
canvas.drawText(Integer.toString(numbers[x][y]), x * width + 50, y * width + 50 - top / 2 - bottom / 2, textPaint);
} else if (numbers[x][y] == -1) {
canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
}
break;
case 2://红色
canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
redCount++;
break;
case 0://灰色
grayCount++;
break;
default:
break;
}
}
} if(grayCount == 0 && redCount == 10) {
Toast.makeText(myContext, "你赢了!", Toast.LENGTH_LONG).show();
}
} @Override
public boolean onTouchEvent(MotionEvent event) {
// super.onTouchEvent(event);
//
// if (event.getAction() == MotionEvent.ACTION_DOWN) {
// leftTop = findLeftTop(event.getX(), event.getY());
// colors[leftTop.x][leftTop.y] = 1;
// this.invalidate();
// }
//
// return true; //使用手势触摸
return detector.onTouchEvent(event);
} //找到触点所在的小方块
private Point findLeftTop(float touchX, float touchY) {
Point point = new Point(); for (int i = 0; i < rowCount; i++) {
if (touchX - i * width > 0 && touchX - i * width < width) {
point.x = i;
}
if (touchY - i * width > 0 && touchY - i * width < width) {
point.y = i;
}
} return point;
} //重置游戏
private void reset() {
colors = new int[rowCount][rowCount];
numbers = new int[rowCount][rowCount]; createMines();
} private void createMines() {
int x;
int y;
int minesCount = 0;
Random random = new Random(); //藏雷
while (minesCount < mineCount) {
x = random.nextInt(rowCount);
y = random.nextInt(rowCount); if (numbers[x][y] != -1) {
numbers[x][y] = -1;
minesCount++;
plusNumber(x, y);
}
}
} //填充雷附近的数字
private void plusNumber (int x, int y) {
//左上
if (x - 1 >= 0 && y - 1 >= 0 && numbers[x - 1][y - 1] != -1) {
numbers[x - 1][y - 1]++;
}
//上
if (y - 1 >= 0 && numbers[x][y - 1] != -1) {
numbers[x][y - 1]++;
}
//右上
if (x + 1 < rowCount && y - 1 >= 0 && numbers[x + 1][y - 1] != -1) {
numbers[x + 1][y - 1]++;
}
//右
if (x + 1 < rowCount && numbers[x + 1][y] != -1) {
numbers[x + 1][y]++;
}
//右下
if (x + 1 < rowCount && y + 1 < rowCount && numbers[x + 1][y + 1] != -1) {
numbers[x + 1][y + 1]++;
}
//下
if (y + 1 < rowCount && numbers[x][y + 1] != -1) {
numbers[x][y + 1]++;
}
//左下
if (x - 1 >= 0 && y + 1 < rowCount && numbers[x - 1][y + 1] != -1) {
numbers[x - 1][y + 1]++;
}
//左
if (x - 1 >= 0 && numbers[x - 1][y] != -1) {
numbers[x - 1][y]++;
}
}
}
接下来主要讲一讲自定义View内部的代码。
这里是通过GestureDetector来实现的区分短按和长按事件,具体实现步骤为:实现GestureDetector.OnGestureListener接口→创建GestureDetector对象(该对象参数为上一步实现类的对象)→在onTouchEvent中调用GestureDetector的onTouchEvent方法
下面说一下监听类的各个方法:
private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
//防止其他事件不执行,所以返回true
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
//短按事件
@Override
public boolean onSingleTapUp(MotionEvent e) {
leftTop = findLeftTop(e.getX(), e.getY());
if (numbers[leftTop.x][leftTop.y] == -1) {
Toast.makeText(myContext, "你输了!", Toast.LENGTH_SHORT).show();
reset();
MineSweepingView.this.invalidate();
} else {
expand(leftTop.x, leftTop.y);
MineSweepingView.this.invalidate();
}
return true;
}
//根据扫雷逻辑展开小方块
private void expand(int x, int y) {
//如果是雷
if (numbers[x][y] == -1) {
return;
} else if (numbers[x][y] == 0 && colors[x][y] == 0) {
colors[x][y] = 1;
//左上
if (x - 1 >= 0 && y - 1 >= 0) {
expand(x - 1, y - 1);
}
//上
if (y - 1 >= 0) {
expand(x, y - 1);
}
//右上
if (x + 1 < rowCount && y - 1 >= 0) {
expand(x + 1, y - 1);
}
//右
if (x + 1 < rowCount) {
expand(x + 1, y);
}
//右下
if (x + 1 < rowCount && y + 1 < rowCount) {
expand(x + 1, y + 1);
}
//下
if (y + 1 < rowCount) {
expand(x, y + 1);
}
//左下
if (x - 1 >= 0 && y + 1 < rowCount) {
expand(x - 1, y + 1);
}
//左
if (x - 1 >= 0) {
expand(x - 1, y);
}
} else {
colors[x][y] = 1;
}
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
//长按事件
@Override
public void onLongPress(MotionEvent e) {
leftTop = findLeftTop(e.getX(), e.getY());
if (colors[leftTop.x][leftTop.y] != 1) {
colors[leftTop.x][leftTop.y] = 2;
MineSweepingView.this.invalidate();
}
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
};
主要的是onSingleTapUp方法以及onLongPress方法,前者是短按事件,后者是长按事件。
先说长按事件,findLeftTop方法用于找到触点所在小方块左上角定点坐标,然后直接把该小方块的颜色改为红色,然后调用invalidate方法用于触发onDraw方法。
短按事件,首先找到触点所在小方块左上角坐标。然后判断该小方块是否为雷,如果是雷,直接重置游戏。否则的话,根据expand的逻辑来展开方块。
expand的逻辑:如果要展开的小方块下为大于零的数字,则展开该方块;如果要展开的小方块下为-1(也就是雷),则直接返回;如果要展开的小方块的数字为零,则展开该方块并将盖方块周围的方块执行expand逻辑。
通过expand方法,就实现了扫雷“展开”的逻辑。
下面说一下onDraw方法:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GRAY);
for (int i = 0; i <= canvas.getWidth(); i += width) {
canvas.drawLine(i, 0, i, canvas.getHeight(), strokePaint);
}
for (int j = 0; j <= canvas.getHeight(); j += width) {
canvas.drawLine(0, j, canvas.getWidth(), j, strokePaint);
}
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float top = fontMetrics.top;
float bottom = fontMetrics.bottom;
int grayCount = 0;
int redCount = 0;
for (int x = 0; x < rowCount; x++) {
for (int y = 0; y < rowCount; y++) {
switch (colors[x][y]) {
//宽高各缩减一单位是为了防止把细线也给覆盖了
case 1://白色
canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, whitePaint);
if (numbers[x][y] != -1 && numbers[x][y] != 0) {
canvas.drawText(Integer.toString(numbers[x][y]), x * width + 50, y * width + 50 - top / 2 - bottom / 2, textPaint);
} else if (numbers[x][y] == -1) {
canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
}
break;
case 2://红色
canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
redCount++;
break;
case 0://灰色
grayCount++;
break;
default:
break;
}
}
}
if(grayCount == 0 && redCount == 10) {
Toast.makeText(myContext, "你赢了!", Toast.LENGTH_LONG).show();
}
}
具体的步骤为:用灰色充当背景色,涂色→画好竖线以及横线,实现了9×9的矩阵→根据小方块颜色来涂色
根据小方块颜色涂色的逻辑为:如果颜色是白色,则先把小方块变成白色,然后在小方块上画上小方块内要显示的数字(如果是零,则不显示);如果颜色是红色,则把小方块颜色涂成红色。
涂色之后可以获取到灰色小方块的个数以及红色小方块的个数。当灰色小方块的个数为零并且红色小方块的个数为十的时候证明游戏成功!
接下来说一下reset方法,该方法生成了雷以及雷周围的数字:
//重置游戏
private void reset() {
colors = new int[rowCount][rowCount];
numbers = new int[rowCount][rowCount]; createMines();
} private void createMines() {
int x;
int y;
int minesCount = 0;
Random random = new Random(); //藏雷
while (minesCount < mineCount) {
x = random.nextInt(rowCount);
y = random.nextInt(rowCount); if (numbers[x][y] != -1) {
numbers[x][y] = -1;
minesCount++;
plusNumber(x, y);
}
}
} //填充雷附近的数字
private void plusNumber (int x, int y) {
//左上
if (x - 1 >= 0 && y - 1 >= 0 && numbers[x - 1][y - 1] != -1) {
numbers[x - 1][y - 1]++;
}
//上
if (y - 1 >= 0 && numbers[x][y - 1] != -1) {
numbers[x][y - 1]++;
}
//右上
if (x + 1 < rowCount && y - 1 >= 0 && numbers[x + 1][y - 1] != -1) {
numbers[x + 1][y - 1]++;
}
//右
if (x + 1 < rowCount && numbers[x + 1][y] != -1) {
numbers[x + 1][y]++;
}
//右下
if (x + 1 < rowCount && y + 1 < rowCount && numbers[x + 1][y + 1] != -1) {
numbers[x + 1][y + 1]++;
}
//下
if (y + 1 < rowCount && numbers[x][y + 1] != -1) {
numbers[x][y + 1]++;
}
//左下
if (x - 1 >= 0 && y + 1 < rowCount && numbers[x - 1][y + 1] != -1) {
numbers[x - 1][y + 1]++;
}
//左
if (x - 1 >= 0 && numbers[x - 1][y] != -1) {
numbers[x - 1][y]++;
}
}
reset方法首先重置了颜色矩阵和数字矩阵。
接下来通过随机数的方式随机生成一个雷,并把数字矩阵下该坐标所对应的值改为-1;接着把该雷周围一圈数字都加一;然后生成第二个雷。
这样循环了十个雷之后雷以及数字就生成完毕。
效果图:



以上就是通过自定义View的方式实现的一个简易版扫雷。
第一次写博客,比较生疏。如有建议,欢迎评论~
利用自定义View实现扫雷游戏的更多相关文章
- 自定义View实现五子棋游戏
成功的路上一点也不拥挤,因为坚持的人太少了. ---简书上看到的一句话 未来请假三天顺带加上十一回家结婚,不得不说真是太坑了,去年婚假还有10天,今年一下子缩水到了3天,只能赶着十一办事了. 最近还在 ...
- iOS开发——UI基础-自定义构造方法,layoutSubviews,Xib文件,利用Xib自定义View
一.自定义构造方法 有时候需要快速创建对象,可以自定义构造方法 + (instancetype)shopView { return [[self alloc] init]; } - (instance ...
- android愤怒小鸟游戏、自定义View、掌上餐厅App、OpenGL自定义气泡、抖音电影滤镜效果等源码
Android精选源码 精练的范围选择器,范围和单位可以自定义 自定义View做的小鸟游戏 android popwindow选择商品规格颜色尺寸效果源码 实现Android带有锯齿背景的优惠样式源码 ...
- Android ——利用OnDraw实现自定义View(转)
自定义View的实现方式大概可以分为三种,自绘控件.组合控件.以及继承控件.本文将介绍自绘控件的用法.自绘控件的意思是,这个控件上的内容是用onDraw函数绘制出来的.关于onDraw函数的介绍可参看 ...
- C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式
C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...
- 【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介
前言 在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指.但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了.多点触控是在Android2 ...
- Android之自定义View的实现
对于学习Android开发的小童鞋对于自定义View一定不会陌生,相信大家对它是又爱又恨,爱它可以跟随我们的心意设计出漂亮的效果:恨它想要完全流畅掌握,需要一定的功夫.对于初学者来说确实很不容易,网上 ...
- [转]Android自定义控件三部曲系列完全解析(动画, 绘图, 自定义View)
来源:http://blog.csdn.net/harvic880925/article/details/50995268 一.自定义控件三部曲之动画篇 1.<自定义控件三部曲之动画篇(一)—— ...
- Android 自定义View合集
自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...
随机推荐
- 利用redis + lua解决抢红包高并发的问题
抢红包的需求分析 抢红包的场景有点像秒杀,但是要比秒杀简单点.因为秒杀通常要和库存相关.而抢红包则可以允许有些红包没有被抢到,因为发红包的人不会有损失,没抢完的钱再退回给发红包的人即可.另外像小米这样 ...
- C++ Opencv remap()重映射函数详解及使用示例
一.重映射及remap()函数介绍 重映射,就是把一幅图像中某位置的像素放置到另一图像指定位置的过程.即: 在重映射过程中,图像的大小也可以同时发生改变.此时像素与像素之间的关系就不是一一对应关系,因 ...
- [Abp 源码分析]二、模块系统
0.简介 整个 Abp 框架由各个模块组成,基本上可以看做一个程序集一个模块,不排除一个程序集有多个模块的可能性.可以看看他官方的这些扩展库: 可以看到每个项目文件下面都会有一个 xxxModule ...
- 深入理解OkHttp源码(三)——网络操作
这篇博客侧重于了解OkHttp的网络部分,包括Socket的创建.连接,连接池等要点.OkHttp对Socket的流操作使用了Okio进行了封装,本篇博客不做介绍,想了解的朋友可以参考拆轮子系列:拆O ...
- Python内置函数(7)——bytearray
英文文档: class bytearray([source[, encoding[, errors]]]) Return a new array of bytes. The bytearray cla ...
- PrismCDN 网络的架构解析,以及低延迟、低成本的奥秘
5 月 19.20 日,行业精英齐聚的 WebRTCon 2018 在上海举办.又拍云 PrismCDN 项目负责人凌建发在大会做了<又拍云低延时的 WebP2P 直播实践>的精彩分享. ...
- 如何阅读jdk源码?
简介 这篇文章主要讲述jdk本身的源码该如何阅读,关于各种框架的源码阅读我们后面再一起探讨. 笔者认为阅读源码主要包括下面几个步骤. 设定目标 凡事皆有目的,阅读源码也是一样. 从大的方面来说,我们阅 ...
- 【Maven】---Nexus私服配置Setting和Pom
maven---nexus私服配置setting和pom 上一遍博客已经在linux服务器上,搭建好nexus私服了,博客地址:Linux搭建Nexus3.X私服 现在就需要配置setting.xml ...
- 『最长等差数列 线性DP』
最长等差数列(51nod 1055) Description N个不同的正整数,找出由这些数组成的最长的等差数列. 例如:1 3 5 6 8 9 10 12 13 14 等差子数列包括(仅包括两项的不 ...
- 小技巧,把Markdown文本发布到微信公众号文章
估计很多人都是这样,平常工作在github,等到有成果要发布,又要写微信公众号. github用Markdown,微信公众号,至少截止今天,还是沿用富文本的方式.不是说富文本不好,但每次精心撰写的内容 ...