利用自定义View实现扫雷游戏
游戏规则:
简单版的扫雷事实上就是一个9×9的矩阵,其中有十个点是雷,非雷方块的数字代表该方块周围八个方块中雷的个数。通过长按某一方块(方块会变红)认定该方块为玩家认为的雷,通过短按某一方块来“展开”该方块。
展开:如果该方块为雷,则游戏失败;如果该方块下为非零数字,则将该方块的数字告诉玩家;如果该方块下的数字为零,则展开该方块周围区域,直到展开到数字或者雷为止。
实现难点:
- 如何生成不重复的十个雷
- 如何生成非雷区域的数字
- 如何实现“展开”
基本思路:
首先定义两个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的方式实现的一个简易版扫雷。
第一次写博客,比较生疏。如有建议,欢迎评论~