利用自定义View实现扫雷游戏

游戏规则:

简单版的扫雷事实上就是一个9×9的矩阵,其中有十个点是雷,非雷方块的数字代表该方块周围八个方块中雷的个数。通过长按某一方块(方块会变红)认定该方块为玩家认为的雷,通过短按某一方块来“展开”该方块。

展开:如果该方块为雷,则游戏失败;如果该方块下为非零数字,则将该方块的数字告诉玩家;如果该方块下的数字为零,则展开该方块周围区域,直到展开到数字或者雷为止。

实现难点:

  1. 如何生成不重复的十个雷
  2. 如何生成非雷区域的数字
  3. 如何实现“展开”

基本思路:

首先定义两个9×9的矩阵,其中一个矩阵用来存放各个方块下的数字(-1代表雷),另一个用来存放该方块的颜色(0代表灰色,即默认色;1代表白色,即普通展开;2代表红色,即认定的雷)。

通过自定义View来实现。并且该自定义View的宽高设置为固定的901px(小方格的边长为100px,线的宽度为1px)。

每点击一次方块都会调用view的invalidate方法,进而会触发onDraw方法。在点击事件中更改颜色矩阵的值,并在onDraw方法中根据两个矩阵的值进行重绘。

代码展示:

  • 布局文件
1 <?xml version="1.0" encoding="utf-8"?>
2 <RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
3     android:layout_width="match_parent"
4     android:layout_height="match_parent">
5     <com.example.lanxingren.minesweeping.MineSweepingView
6         android:layout_width= "901px"
7         android:layout_height="901px"
8         android:layout_centerInParent="true"/>
9 </RelativeLayout>
  • 主活动
 1 package com.example.lanxingren.minesweeping;
 2 
 3 import android.support.v7.app.AppCompatActivity;
 4 import android.os.Bundle;
 5 
 6 public class MainActivity extends AppCompatActivity {
 7 
 8     @Override
 9     protected void onCreate(Bundle savedInstanceState) {
10         super.onCreate(savedInstanceState);
11         setContentView(R.layout.activity_main);
12     }
13 }
  • 自定义View
  1 package com.example.lanxingren.minesweeping;
  2 
  3 import android.content.Context;
  4 import android.graphics.Canvas;
  5 import android.graphics.Color;
  6 import android.graphics.Paint;
  7 import android.graphics.Point;
  8 import android.graphics.Typeface;
  9 import android.util.AttributeSet;
 10 import android.view.GestureDetector;
 11 import android.view.MotionEvent;
 12 import android.view.View;
 13 import android.widget.Toast;
 14 
 15 import java.util.Random;
 16 
 17 
 18 public class MineSweepingView extends View {
 19 
 20     //    private MineSweepingView (Context context) {
 21     //        super(context);
 22     //    }
 23 
 24     Context myContext;
 25 
 26 
 27     // 触摸方块左上角的点
 28     Point leftTop;
 29 
 30     // 默认背景画笔
 31     Paint strokePaint = new Paint();
 32 
 33     // 涂色画笔,红色代表玩家认为的雷,白色代表展开该方块
 34     Paint whitePaint = new Paint();
 35     Paint redPaint = new Paint();
 36 
 37     //绘制数字的画笔
 38     Paint textPaint = new Paint();
 39 
 40     // 代表每个坐标的颜色,其中0代表银灰色,1代表白色,2代表红色
 41     int[][] colors;
 42     // 代表每个坐标的数字,其中-1代表雷
 43     int[][] numbers;
 44 
 45     //小格子边长
 46     final int width = 100;
 47 
 48     //一行格子数
 49     final int rowCount = 9;
 50 
 51     //雷的个数
 52     final int mineCount = 10;
 53 
 54     //手势操作监听器
 55     private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
 56         //防止其他事件不执行,所以返回true
 57         @Override
 58         public boolean onDown(MotionEvent e) {
 59             return true;
 60         }
 61 
 62         @Override
 63         public void onShowPress(MotionEvent e) {
 64 
 65         }
 66 
 67         //短按事件
 68         @Override
 69         public boolean onSingleTapUp(MotionEvent e) {
 70             leftTop = findLeftTop(e.getX(), e.getY());
 71 
 72             if (numbers[leftTop.x][leftTop.y] == -1) {
 73                 Toast.makeText(myContext, "你输了!", Toast.LENGTH_SHORT).show();
 74                 reset();
 75                 MineSweepingView.this.invalidate();
 76             } else {
 77                 expand(leftTop.x, leftTop.y);
 78                 MineSweepingView.this.invalidate();
 79             }
 80 
 81             return true;
 82         }
 83 
 84         //根据扫雷逻辑展开小方块
 85         private void expand(int x, int y) {
 86             //如果是雷
 87             if (numbers[x][y] == -1) {
 88                 return;
 89             } else if (numbers[x][y] == 0 && colors[x][y] == 0) {
 90                 colors[x][y] = 1;
 91 
 92                 //左上
 93                 if (x - 1 >= 0 && y - 1 >= 0) {
 94                     expand(x - 1, y - 1);
 95                 }
 96                 //
 97                 if (y - 1 >= 0) {
 98                     expand(x, y - 1);
 99                 }
100                 //右上
101                 if (x + 1 < rowCount && y - 1 >= 0) {
102                     expand(x + 1, y - 1);
103                 }
104                 //
105                 if (x + 1 < rowCount) {
106                     expand(x + 1, y);
107                 }
108                 //右下
109                 if (x + 1 < rowCount && y + 1 < rowCount) {
110                     expand(x + 1, y + 1);
111                 }
112                 //
113                 if (y + 1 < rowCount) {
114                     expand(x, y + 1);
115                 }
116                 //左下
117                 if (x - 1 >= 0 && y + 1 < rowCount) {
118                     expand(x - 1, y + 1);
119                 }
120                 //
121                 if (x - 1 >= 0) {
122                     expand(x - 1, y);
123                 }
124             } else {
125                 colors[x][y] = 1;
126             }
127 
128         }
129 
130         @Override
131         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
132             return false;
133         }
134 
135         //长按事件
136         @Override
137         public void onLongPress(MotionEvent e) {
138             leftTop = findLeftTop(e.getX(), e.getY());
139 
140             if (colors[leftTop.x][leftTop.y] != 1) {
141                 colors[leftTop.x][leftTop.y] = 2;
142                 MineSweepingView.this.invalidate();
143             }
144         }
145 
146         @Override
147         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
148             return false;
149         }
150     };
151 
152     private GestureDetector detector = new GestureDetector(onGestureListener);
153 
154     public MineSweepingView(Context context, AttributeSet attributeSet) {
155         super(context, attributeSet);
156 
157         myContext = context;
158 
159         strokePaint.setColor(Color.BLACK);
160         strokePaint.setStrokeWidth(1);
161 
162         whitePaint.setStyle(Paint.Style.FILL_AND_STROKE);
163         whitePaint.setColor(Color.WHITE);
164 
165         redPaint.setStyle(Paint.Style.FILL_AND_STROKE);
166         redPaint.setColor(Color.RED);
167 
168         textPaint.setColor(Color.BLACK);
169         textPaint.setTextAlign(Paint.Align.CENTER);
170         textPaint.setTextSize(50);
171         Typeface typeface = Typeface.createFromAsset(context.getAssets(), "fonts/consola.ttf");
172         textPaint.setTypeface(typeface);
173         textPaint.setStyle(Paint.Style.FILL);
174 
175         reset();
176     }
177 
178     @Override
179     protected void onDraw(Canvas canvas) {
180         super.onDraw(canvas);
181 
182         canvas.drawColor(Color.GRAY);
183 
184         for (int i = 0; i <= canvas.getWidth(); i += width) {
185             canvas.drawLine(i, 0, i, canvas.getHeight(), strokePaint);
186         }
187 
188         for (int j = 0; j <= canvas.getHeight(); j += width) {
189             canvas.drawLine(0, j, canvas.getWidth(), j, strokePaint);
190         }
191 
192         Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
193         float top = fontMetrics.top;
194         float bottom = fontMetrics.bottom;
195 
196         int grayCount = 0;
197         int redCount = 0;
198 
199         for (int x = 0; x < rowCount; x++) {
200             for (int y = 0; y < rowCount; y++) {
201                 switch (colors[x][y]) {
202                     //宽高各缩减一单位是为了防止把细线也给覆盖了
203                     case 1://白色
204                         canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, whitePaint);
205                         if (numbers[x][y] != -1 && numbers[x][y] != 0) {
206                             canvas.drawText(Integer.toString(numbers[x][y]), x * width + 50, y * width + 50 - top / 2 - bottom / 2, textPaint);
207                         } else if (numbers[x][y] == -1) {
208                             canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
209                         }
210                         break;
211                     case 2://红色
212                         canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
213                         redCount++;
214                         break;
215                     case 0://灰色
216                         grayCount++;
217                         break;
218                     default:
219                         break;
220                 }
221             }
222         }
223 
224         if(grayCount == 0 && redCount == 10) {
225             Toast.makeText(myContext, "你赢了!", Toast.LENGTH_LONG).show();
226         }
227     }
228 
229     @Override
230     public boolean onTouchEvent(MotionEvent event) {
231 //        super.onTouchEvent(event);
232 //
233 //        if (event.getAction() == MotionEvent.ACTION_DOWN) {
234 //            leftTop = findLeftTop(event.getX(), event.getY());
235 //            colors[leftTop.x][leftTop.y] = 1;
236 //            this.invalidate();
237 //        }
238 //
239 //        return true;
240 
241         //使用手势触摸
242         return detector.onTouchEvent(event);
243     }
244 
245     //找到触点所在的小方块
246     private Point findLeftTop(float touchX, float touchY) {
247         Point point = new Point();
248 
249         for (int i = 0; i < rowCount; i++) {
250             if (touchX - i * width > 0 && touchX - i * width < width) {
251                 point.x = i;
252             }
253             if (touchY - i * width > 0 && touchY - i * width < width) {
254                 point.y = i;
255             }
256         }
257 
258         return point;
259     }
260 
261     //重置游戏
262     private void reset() {
263         colors = new int[rowCount][rowCount];
264         numbers = new int[rowCount][rowCount];
265 
266         createMines();
267     }
268 
269     private void createMines() {
270         int x;
271         int y;
272         int minesCount = 0;
273         Random random = new Random();
274 
275         //藏雷
276         while (minesCount < mineCount) {
277             x = random.nextInt(rowCount);
278             y = random.nextInt(rowCount);
279 
280             if (numbers[x][y] != -1) {
281                 numbers[x][y] = -1;
282                 minesCount++;
283                 plusNumber(x, y);
284             }
285         }
286     }
287 
288     //填充雷附近的数字
289     private void plusNumber (int x, int y) {
290         //左上
291         if (x - 1 >= 0 && y - 1 >= 0 && numbers[x - 1][y - 1] != -1) {
292             numbers[x - 1][y - 1]++;
293         }
294         //
295         if (y - 1 >= 0 && numbers[x][y - 1] != -1) {
296             numbers[x][y - 1]++;
297         }
298         //右上
299         if (x + 1 < rowCount && y - 1 >= 0 && numbers[x + 1][y - 1] != -1) {
300             numbers[x + 1][y - 1]++;
301         }
302         //
303         if (x + 1 < rowCount && numbers[x + 1][y] != -1) {
304             numbers[x + 1][y]++;
305         }
306         //右下
307         if (x + 1 < rowCount && y + 1 < rowCount && numbers[x + 1][y + 1] != -1) {
308             numbers[x + 1][y + 1]++;
309         }
310         //
311         if (y + 1 < rowCount && numbers[x][y + 1] != -1) {
312             numbers[x][y + 1]++;
313         }
314         //左下
315         if (x - 1 >= 0 && y + 1 < rowCount && numbers[x - 1][y + 1] != -1) {
316             numbers[x - 1][y + 1]++;
317         }
318         //
319         if (x - 1 >= 0 && numbers[x - 1][y] != -1) {
320             numbers[x - 1][y]++;
321         }
322     }
323 }

 

接下来主要讲一讲自定义View内部的代码。

 

这里是通过GestureDetector来实现的区分短按和长按事件,具体实现步骤为:实现GestureDetector.OnGestureListener接口→创建GestureDetector对象(该对象参数为上一步实现类的对象)→在onTouchEvent中调用GestureDetector的onTouchEvent方法

 

下面说一下监听类的各个方法:

 1 private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
 2         //防止其他事件不执行,所以返回true
 3         @Override
 4         public boolean onDown(MotionEvent e) {
 5             return true;
 6         }
 7 
 8         @Override
 9         public void onShowPress(MotionEvent e) {
10 
11         }
12 
13         //短按事件
14         @Override
15         public boolean onSingleTapUp(MotionEvent e) {
16             leftTop = findLeftTop(e.getX(), e.getY());
17 
18             if (numbers[leftTop.x][leftTop.y] == -1) {
19                 Toast.makeText(myContext, "你输了!", Toast.LENGTH_SHORT).show();
20                 reset();
21                 MineSweepingView.this.invalidate();
22             } else {
23                 expand(leftTop.x, leftTop.y);
24                 MineSweepingView.this.invalidate();
25             }
26 
27             return true;
28         }
29 
30         //根据扫雷逻辑展开小方块
31         private void expand(int x, int y) {
32             //如果是雷
33             if (numbers[x][y] == -1) {
34                 return;
35             } else if (numbers[x][y] == 0 && colors[x][y] == 0) {
36                 colors[x][y] = 1;
37 
38                 //左上
39                 if (x - 1 >= 0 && y - 1 >= 0) {
40                     expand(x - 1, y - 1);
41                 }
42                 //
43                 if (y - 1 >= 0) {
44                     expand(x, y - 1);
45                 }
46                 //右上
47                 if (x + 1 < rowCount && y - 1 >= 0) {
48                     expand(x + 1, y - 1);
49                 }
50                 //
51                 if (x + 1 < rowCount) {
52                     expand(x + 1, y);
53                 }
54                 //右下
55                 if (x + 1 < rowCount && y + 1 < rowCount) {
56                     expand(x + 1, y + 1);
57                 }
58                 //
59                 if (y + 1 < rowCount) {
60                     expand(x, y + 1);
61                 }
62                 //左下
63                 if (x - 1 >= 0 && y + 1 < rowCount) {
64                     expand(x - 1, y + 1);
65                 }
66                 //
67                 if (x - 1 >= 0) {
68                     expand(x - 1, y);
69                 }
70             } else {
71                 colors[x][y] = 1;
72             }
73 
74         }
75 
76         @Override
77         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
78             return false;
79         }
80 
81         //长按事件
82         @Override
83         public void onLongPress(MotionEvent e) {
84             leftTop = findLeftTop(e.getX(), e.getY());
85 
86             if (colors[leftTop.x][leftTop.y] != 1) {
87                 colors[leftTop.x][leftTop.y] = 2;
88                 MineSweepingView.this.invalidate();
89             }
90         }
91 
92         @Override
93         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
94             return false;
95         }
96     };

 

主要的是onSingleTapUp方法以及onLongPress方法,前者是短按事件,后者是长按事件。

先说长按事件,findLeftTop方法用于找到触点所在小方块左上角定点坐标,然后直接把该小方块的颜色改为红色,然后调用invalidate方法用于触发onDraw方法。

短按事件,首先找到触点所在小方块左上角坐标。然后判断该小方块是否为雷,如果是雷,直接重置游戏。否则的话,根据expand的逻辑来展开方块。

expand的逻辑:如果要展开的小方块下为大于零的数字,则展开该方块;如果要展开的小方块下为-1(也就是雷),则直接返回;如果要展开的小方块的数字为零,则展开该方块并将盖方块周围的方块执行expand逻辑。

通过expand方法,就实现了扫雷“展开”的逻辑。

 

下面说一下onDraw方法:

 1 protected void onDraw(Canvas canvas) {
 2         super.onDraw(canvas);
 3 
 4         canvas.drawColor(Color.GRAY);
 5 
 6         for (int i = 0; i <= canvas.getWidth(); i += width) {
 7             canvas.drawLine(i, 0, i, canvas.getHeight(), strokePaint);
 8         }
 9 
10         for (int j = 0; j <= canvas.getHeight(); j += width) {
11             canvas.drawLine(0, j, canvas.getWidth(), j, strokePaint);
12         }
13 
14         Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
15         float top = fontMetrics.top;
16         float bottom = fontMetrics.bottom;
17 
18         int grayCount = 0;
19         int redCount = 0;
20 
21         for (int x = 0; x < rowCount; x++) {
22             for (int y = 0; y < rowCount; y++) {
23                 switch (colors[x][y]) {
24                     //宽高各缩减一单位是为了防止把细线也给覆盖了
25                     case 1://白色
26                         canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, whitePaint);
27                         if (numbers[x][y] != -1 && numbers[x][y] != 0) {
28                             canvas.drawText(Integer.toString(numbers[x][y]), x * width + 50, y * width + 50 - top / 2 - bottom / 2, textPaint);
29                         } else if (numbers[x][y] == -1) {
30                             canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
31                         }
32                         break;
33                     case 2://红色
34                         canvas.drawRect(x * width + 1, y * width + 1, (x + 1) * width - 1, (y + 1) * width - 1, redPaint);
35                         redCount++;
36                         break;
37                     case 0://灰色
38                         grayCount++;
39                         break;
40                     default:
41                         break;
42                 }
43             }
44         }
45 
46         if(grayCount == 0 && redCount == 10) {
47             Toast.makeText(myContext, "你赢了!", Toast.LENGTH_LONG).show();
48         }
49     }

 

具体的步骤为:用灰色充当背景色,涂色→画好竖线以及横线,实现了9×9的矩阵→根据小方块颜色来涂色

根据小方块颜色涂色的逻辑为:如果颜色是白色,则先把小方块变成白色,然后在小方块上画上小方块内要显示的数字(如果是零,则不显示);如果颜色是红色,则把小方块颜色涂成红色。

涂色之后可以获取到灰色小方块的个数以及红色小方块的个数。当灰色小方块的个数为零并且红色小方块的个数为十的时候证明游戏成功!

 

接下来说一下reset方法,该方法生成了雷以及雷周围的数字:

 1 //重置游戏
 2     private void reset() {
 3         colors = new int[rowCount][rowCount];
 4         numbers = new int[rowCount][rowCount];
 5 
 6         createMines();
 7     }
 8 
 9     private void createMines() {
10         int x;
11         int y;
12         int minesCount = 0;
13         Random random = new Random();
14 
15         //藏雷
16         while (minesCount < mineCount) {
17             x = random.nextInt(rowCount);
18             y = random.nextInt(rowCount);
19 
20             if (numbers[x][y] != -1) {
21                 numbers[x][y] = -1;
22                 minesCount++;
23                 plusNumber(x, y);
24             }
25         }
26     }
27 
28     //填充雷附近的数字
29     private void plusNumber (int x, int y) {
30         //左上
31         if (x - 1 >= 0 && y - 1 >= 0 && numbers[x - 1][y - 1] != -1) {
32             numbers[x - 1][y - 1]++;
33         }
34         //
35         if (y - 1 >= 0 && numbers[x][y - 1] != -1) {
36             numbers[x][y - 1]++;
37         }
38         //右上
39         if (x + 1 < rowCount && y - 1 >= 0 && numbers[x + 1][y - 1] != -1) {
40             numbers[x + 1][y - 1]++;
41         }
42         //
43         if (x + 1 < rowCount && numbers[x + 1][y] != -1) {
44             numbers[x + 1][y]++;
45         }
46         //右下
47         if (x + 1 < rowCount && y + 1 < rowCount && numbers[x + 1][y + 1] != -1) {
48             numbers[x + 1][y + 1]++;
49         }
50         //
51         if (y + 1 < rowCount && numbers[x][y + 1] != -1) {
52             numbers[x][y + 1]++;
53         }
54         //左下
55         if (x - 1 >= 0 && y + 1 < rowCount && numbers[x - 1][y + 1] != -1) {
56             numbers[x - 1][y + 1]++;
57         }
58         //
59         if (x - 1 >= 0 && numbers[x - 1][y] != -1) {
60             numbers[x - 1][y]++;
61         }
62     }

 

reset方法首先重置了颜色矩阵和数字矩阵。

接下来通过随机数的方式随机生成一个雷,并把数字矩阵下该坐标所对应的值改为-1;接着把该雷周围一圈数字都加一;然后生成第二个雷。

这样循环了十个雷之后雷以及数字就生成完毕。

 

效果图:

 

 

以上就是通过自定义View的方式实现的一个简易版扫雷。

第一次写博客,比较生疏。如有建议,欢迎评论~

posted @ 2018-05-29 15:07  懒星人  阅读(886)  评论(0编辑  收藏  举报