利用自定义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 sortedset实现元素自动过期
这里的自动过期,Redis并没有提供相应的api,但是可以使用一下方法来实现. 需求背景: 给用户返回的文章要求七日内不能重复:文章是存放在java list里边:(这一块就是从db将文章拿出来,然后 ...
- 推荐几个牛逼的 IDEA 插件,还带动图!
阅读本文大概需要 2.3 分钟. 作者:纪莫, cnblogs.com/jimoer 这里只是推荐一下好用的插件,具体的使用方法不一一详细介绍. JRebel for IntelliJ 一款热部署插件 ...
- 导出excel表格,前端和后台导出
问题:项目框架中导出不知道有什么限制,数据稍微大点导出不了,向上面请求解决,结果一圈推下来又推回来了,所以决定自己写,参考了网上很多大神的博客,开始试了前端导出,想着比较简单,但是乱码问题始终解决不了 ...
- [Swift]LeetCode438. 找到字符串中所有字母异位词 | Find All Anagrams in a String
Given a string s and a non-empty string p, find all the start indices of p's anagrams in s. Strings ...
- [Swift]LeetCode812. 最大三角形面积 | Largest Triangle Area
You have a list of points in the plane. Return the area of the largest triangle that can be formed b ...
- PowerShell 中 RunspacePool 执行异步多线程任务
在 PowerShell 中要执行任务脚本,现在通常使用 Runspace,效率很高:任务比较多时,用 Runspace pool 来执行异步操作,可以控制资源池数量,就像 C# 中的线程池一样 == ...
- MySQL开启远程连接权限
对于我们刚开始安装的mysql或者mariadb来说,默认是不开启远程连接的.所以需要我们手动开启远程连接的权限.如果你是使用docker安装mysql那需要先进入容器中,这里就不讲如何进入容器了,百 ...
- Guava 源码分析(Cache 原理)
前言 Google 出的 Guava 是 Java 核心增强的库,应用非常广泛. 我平时用的也挺频繁,这次就借助日常使用的 Cache 组件来看看 Google 大牛们是如何设计的. 缓存 本次主要讨 ...
- 从锅炉工到AI专家(11)(END)
语音识别 TensorFlow 1.x中提供了一个语音识别的例子speech_commands,用于识别常用的命令词汇,实现对设备的语音控制.speech_commands是一个很成熟的语音识别原型, ...
- .net好好地利用Conditional属性
Conditional是.net提供关于编译的属性描述,其作用是添加到方法或属上,通过定义编译符的方式告指示编译器应忽略方法调用或属性.在.NET中Debug 和 Trace 类中的方法都添加了这属性 ...