源码参考文档:

开始以为直接看源码很简单,大概思考了过程,就参考源码自己写程序,但中间出现了很多问题。终究是“纸上得来终觉浅,绝知此事要躬行”。以后一定不能手懒,要多动手。

 

源码分析:

Block类(继承自Button类):即点击时的每个小方块。主要属性包括:

public class Block extends Button {
	private boolean isCovered;				//是否被覆盖(未被打开)的标记
	private boolean isMined;				//是否有雷
	private boolean isFlagged;				//是否被标记(没有标记,或标记为有雷)
	private boolean isQuestionMarked;		//是否被标记为问号
	private boolean isClickable;			//是否可点击(如果是空格,点击过则不可被点击,如果是雷,或者问号,则点击后仍可被点击)
	private int numberOfMineInSurrounding;  //如果是数字,则其周围相邻中含有雷的数目

这里主要说明一下isFlagged与isQuestionMarked的作用:当单击方块时,表示确定该方块为数字,当长按某个未曾打开的方块时,表示对该方块进行标记,isFlagged为ture:长按isFlagged为true的方块时,该方块会显示“?”,isQuestionMarked变为true,表示不确定该方块状态,当再长按isQuestionMarked为true的方块时,其恢复为初始状态。

 

这里还有两个名字很相似,很容易混淆的方法:

public void setNumberOfMinesInSurrounding(int number) {
		 this.numberOfMineInSurrounding = number;
	}
public void setNumberOfMineInSurrounding(int numberOfMineInSurrounding) {
		this.setBackgroundResource(R.drawable.square_grey);
		updateNumber(numberOfMineInSurrounding);
	}

第一个为设置周围雷个数的函数,第二个是对方格进行背景变色即显示数字的设置,二者太相似了,极容易出错。在函数命名方面也需要注意。

 

 

主函数:

主要分为方格区显示、分布雷、为方格添加单击、长击监听事件、检验游戏是否成功或者失败、定时器显示、所需找到雷的个数显示等部分。如果哪位想自己动手写一下改程序,建议分模块逐步完善。否则后期代码的调试会非常麻烦。这里把写代码中遇到的错误贴出来:

 

在方块显示中,开始显示一直

Android studio扫描无用代码无效 android studio扫雷代码_android

不完全,如左上所示:

后来发现问题在showMineField()的改行代码中,

mineField.addView(tableRow, new LayoutParams((blockDimension + 2 * blockPadding) * numberOfColumnsInMineField ,
     blockDimension + 2 * blockPadding));应该为:

 mineField.addView(tableRow, new TableLayout.LayoutParams((blockDimension + 2 * blockPadding) * numberOfColumnsInMineField ,
     blockDimension + 2 * blockPadding));

混淆了LayoutParams与TableLayout.LayoutParams;

BUG 2:Android   Unable to start activity CompnentInfo:java.lang.NullPointerException

以下是该BUG出现的可能原因: 

错误信息字符串:java.lang.RuntimeException: Unable to start activity ComponentInfo{com.first/com.first.Game}: java.lang.NullPointerException

一般都会在Activity  onCreate()方法里的setContentView(XXX)发生此错误,网上查阅了很多原因,大概有四种重要可能的原因:

原因一:xxx的错误,若为R.layout.main  那么应该是main.xml文件中的标签 使用错误,最常见的而且编译器不会提示的错误就是 android:name 和 android:id 两者混淆,仔细检查main.xml的标签是否全部正确

原因二:在setContentView(view)方法之后使用了requestWindowFeature()方法,并且在此错误下面会提示requestFeature必须在setContentView之前使用,只需要把requestWindowFeature()方法放在setContentView(view)方法之前就可以解决

原因三:在onCreate()方法之外,并且不属于任何一个方法体内直接给某控件findById(R.id.xx)所导致,需要在某方法内并且在setContentView(view)方法之前进行findById(R.id.xx)即可解决

原因四:在setContentView(view)之前没有对view进行实例化,只进行了声明而直接 setContentView(view) 所导致,仔细检查view是否setContentView(view)调用之前并在方法内进行实例化即可解决;

 

最后分析是有部件没有初始化导致的。

 BUG  3

:threadid=1: thread exiting with uncaught exception (group=0x40015560)
java.lang.ArithmeticException: divide by zero
at android.widget.TableLayout.mutateColumnsWidth(TableLayout.java:579)

该BUG没有解决。当时是先看的代码,就把整个程序比着写下来了,导致调试困难,后来
逐个模块进行写,调试,没有遇到该问题。隐藏的bug.希望知道的大神多多指导。

 

 

以下是源码即注释,鉴于原文及翻译已经非常清楚了,分析在这里就省略了。。。。。。

 

/***************************************************************
	
	Copyright (C) 2011 by foolstudio. All rights reserved.
	
	本源文件中代码不得用于商业用途,作者保留所有权。
	
	
***************************************************************/

package foolstudio.demo.app.HelloAndroid;


import java.util.Random;

import android.app.Activity;
import android.graphics.Typeface;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.TableRow.LayoutParams;

public class HelloAndroidAct extends Activity {
	
	private TextView txtMineCount;
	private TextView txtTimer;
	private ImageButton btnSmile;

	private TableLayout mineField; // table layout to add mines to

	private Block blocks[][]; // blocks for mine field	
	private int blockDimension = 24; // width of each block
	private int blockPadding = 2; // padding between blocks

	private int numberOfRowsInMineField = 9;
	private int numberOfColumnsInMineField = 9;
	private int totalNumberOfMines = 10;

	// timer to keep track of time elapsed
	private Handler timer = new Handler();
	private int secondsPassed = 0;

	private boolean isTimerStarted; // check if timer already started or not
	private boolean areMinesSet; // check if mines are planted in blocks
	private boolean isGameOver;
	private int minesToFind; // number of mines yet to be discovered
	
    @Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.mine);
		
		txtMineCount = (TextView) findViewById(R.id.MineCount);
		txtTimer = (TextView) findViewById(R.id.Timer);
		
		// set font style for timer and mine count to LCD style
		Typeface lcdFont = Typeface.createFromAsset(getAssets(),
				"fonts/lcd2mono.ttf");
		txtMineCount.setTypeface(lcdFont);
		txtTimer.setTypeface(lcdFont);
		
		btnSmile = (ImageButton) findViewById(R.id.Simely);
		btnSmile.setOnClickListener(new OnClickListener()
		{
			@Override
			public void onClick(View view)
			{
				endCurrentGame();
				startNewGame();
			}
		});
		
		mineField = (TableLayout)findViewById(R.id.MineField);
		showDialog("Click smiley to start New Game", 2000, true, false);
	}
    
    
    private void createMineField()
	{
		// we take one row extra row for each side
		// overall two extra rows and two extra columns
		// first and last row/column are used for calculations purposes only
		//	 x|xxxxxxxxxxxxxx|x
		//	 ------------------
		//	 x|              |x
		//	 x|              |x
		//	 ------------------
		//	 x|xxxxxxxxxxxxxx|x
		// the row and columns marked as x are just used to keep counts of near by mines

		blocks = new Block[numberOfRowsInMineField + 2][numberOfColumnsInMineField + 2];

		for (int row = 0; row < numberOfRowsInMineField + 2; row++)
		{
			for (int column = 0; column < numberOfColumnsInMineField + 2; column++)
			{	
				blocks[row][column] = new Block(this);
				blocks[row][column].setDefaults();

				// pass current row and column number as final int's to event listeners
				// this way we can ensure that each event listener is associated to 
				// particular instance of block only
				final int currentRow = row;
				final int currentColumn = column;

				// add Click Listener
				// this is treated as Left Mouse click
				blocks[row][column].setOnClickListener(new OnClickListener()
				{
					@Override
					public void onClick(View view)
					{
				        if(!isTimerStarted){
				        	startTimer();
							isTimerStarted = true;
				        }
						if(!areMinesSet){
							setMines(currentRow, currentColumn);
							areMinesSet = true;
					       }
						//如果未被标记,则打开该键及周围可以被打开的键
						if(!blocks[currentRow][currentColumn].isFlagged()){
							rippleUncover(currentRow, currentColumn);
							
							if(blocks[currentRow][currentColumn].hasMine()){
								loseGame(currentRow , currentColumn);
							}
							
							if(checkWin()){
								winGame();
							}
						}
					}
				});
				
				blocks[row][column].setOnLongClickListener(new OnLongClickListener(){
					
					@Override
					public boolean onLongClick(View view){
						  if(!isTimerStarted){
					        	startTimer();
								isTimerStarted = true;
					        }
						//Case 1:已被打开的数字 + 未被标记 + 游戏没有结束
						if(!blocks[currentRow][currentColumn].isCovered() && blocks[currentRow][currentColumn].getNumberOfMineInSurrounding() > 0
								&& !isGameOver){
							int nearbyFlaggedBlocks = 0;
							for (int previousRow = -1; previousRow < 2; previousRow++)
							{
								for (int previousColumn = -1; previousColumn < 2; previousColumn++)
								{
									if (blocks[currentRow + previousRow][currentColumn + previousColumn].isFlagged())
									{
										nearbyFlaggedBlocks++;
									}
								}
							}			
							if(blocks[currentRow][currentColumn].getNumberOfMineInSurrounding() == nearbyFlaggedBlocks){
								for (int previousRow = -1; previousRow < 2; previousRow++){
									for (int previousColumn = -1; previousColumn < 2; previousColumn++){
										
										if(!blocks[currentRow + previousRow][currentColumn + previousColumn].isFlagged()){
								
											rippleUncover(currentRow + previousRow, currentColumn + previousColumn);
											
											if(blocks[currentRow + previousRow][currentColumn + previousColumn].hasMine()){
												loseGame(currentRow, currentColumn);
											}
											
											if(checkWin()){
												winGame();
											}
										}
									}
								}
						}
						return true;
					}
						//Case 2: 该按键是空格或者F或者?,进行三者间的转变
						//未标记也未加问号
						else if(blocks[currentRow][currentColumn].isClickable() && blocks[currentRow][currentColumn].isEnabled()
								|| blocks[currentRow][currentColumn].isFlagged()){
							if(!blocks[currentRow][currentColumn].isFlagged() && !blocks[currentRow][currentColumn].isQuestionMarked()){
								blocks[currentRow][currentColumn].setBlockAsDisabled(false);
								blocks[currentRow][currentColumn].setFlagged(true);
								blocks[currentRow][currentColumn].setFlagIcon(true);
								
								minesToFind--;
								updateLeftMines();
							}
							//是F状态,转向?
							else if(!blocks[currentRow][currentColumn].isQuestionMarked()){
								blocks[currentRow][currentColumn].setBlockAsDisabled(true);
								blocks[currentRow][currentColumn].setFlagged(false);
								blocks[currentRow][currentColumn].setQuestionMarked(true);
								blocks[currentRow][currentColumn].setQuestionMarkIcon(true);
								
								minesToFind++;
								updateLeftMines();
							}
							//?转向空白
							else{
								blocks[currentRow][currentColumn].setBlockAsDisabled(true);
								blocks[currentRow][currentColumn].setQuestionMarked(false);
								//blocks[currentRow][currentColumn].setText("");
								blocks[currentRow][currentColumn].clearAllIcons();
								
								if(blocks[currentRow][currentColumn].isFlagged()){
									minesToFind++;
									updateLeftMines();
								}
								blocks[currentRow][currentColumn].setFlagged(false);
							}
							updateLeftMines();
							
						}
						return true;
				}
				});
			}
		}
	}
    
    
    private void winGame(){
    	stopTimer();
    	showDialog("Congratuations~~ You win in " + Integer.toString(this.secondsPassed) + "seconds.", 2000, false, true);
        	for(int row = 1; row < this.numberOfRowsInMineField + 1; ++row){
        		for(int column = 1; column < this.numberOfColumnsInMineField + 1; ++column){
        			//包含雷并已被标记
        			if(blocks[row][column].hasMine() && !blocks[row][column].isFlagged()){
        				blocks[row][column].setBlockAsDisabled(false);
        				blocks[row][column].setMineIcon(true);
        			}
        			//有雷但没有标记
        			else if(blocks[row][column].hasMine() && blocks[row][column].isFlagged()){
        				blocks[row][column].setFlagIcon(true);
        			}//包含数字或者空格
        			else{
        				blocks[row][column].openBlock();
        			}
        		}
        	}
    }
    
    private void updateLeftMines(){
    	if(this.minesToFind < 10){
    		this.txtMineCount.setText("00" + Integer.toString(this.minesToFind));
    	}
    	else if(this.minesToFind < 10){
    		this.txtMineCount.setText("0" + Integer.toString(this.minesToFind));
    	}
    	else{
    		this.txtMineCount.setText(Integer.toString(this.minesToFind));
    	}
    }
    
    private boolean checkWin(){
    	for(int row = 1; row < this.numberOfRowsInMineField + 1; ++row){
    		for(int column = 1; column < this.numberOfColumnsInMineField + 1; ++column){
    			//判断条件:没有雷并且已经被打开
    			if(!blocks[row][column].hasMine() && blocks[row][column].isCovered()){
    				return false;
    			}
    		}
    	}
    	return true;
    }
    
    private void loseGame(int currentRow , int currentColumn) {
		// TODO Auto-generated method stub
    	isGameOver = true;
    	stopTimer();
    	btnSmile.setBackgroundResource(R.drawable.sad);
    	this.isTimerStarted = false;
    	
    	for(int row = 1; row < numberOfRowsInMineField + 1; row++){
			for(int column = 1; column < numberOfColumnsInMineField + 1; column++){
				blocks[row][column].setBlockAsDisabled(false);
				if(blocks[row][column].hasMine() && !blocks[row][column].isFlagged()){
					blocks[row][column].setMineIcon(false);
				}
				if(!blocks[row][column].hasMine() && blocks[row][column].isFlagged()){
					blocks[row][column].setFlagged(false);
				}
				if(blocks[row][column].isFlagged()){
					blocks[row][column].setClickable(false);
				}
			}
		}
    	
    	blocks[currentRow][currentColumn].triggerMine();
    	
    	showDialog("Game Over in"+ Integer.toString(this.secondsPassed)+"seconds.Welcome to come again~~", 2000, false, false);
	}
    
    //类似于重新开始Restart
	private void endCurrentGame() {
		// TODO Auto-generated method stub
		stopTimer();
		isGameOver = true;
		areMinesSet = false;
		isTimerStarted = false;
		minesToFind = this.totalNumberOfMines;
		
		this.secondsPassed = 0;
		btnSmile.setBackgroundResource(R.drawable.smile);
		
		this.mineField.removeAllViews();
		
	}
    
	private void startNewGame(){
		isGameOver = false;
		areMinesSet = false;
		isTimerStarted = false;
		minesToFind = this.totalNumberOfMines;
		
		this.createMineField();
		this.showMineField();
		
	}
   public void rippleUncover(int rowClicked, int columnClicked){
	   if(!blocks[rowClicked][columnClicked].isClickable() ||
			   blocks[rowClicked][columnClicked].isFlagged() ){
		   return ;
	   }
	   blocks[rowClicked][columnClicked].openBlock();
	   
	   if(blocks[rowClicked][columnClicked].getNumberOfMineInSurrounding() != 0){
		   return;
	   }
	   
	   for(int prerow = -1; prerow < 2; prerow++ ){
		   for(int precolumn = -1; precolumn < 2; precolumn++){
			   if(blocks[rowClicked + prerow][columnClicked + precolumn].isCovered() && ((rowClicked + prerow) > 0 ) &&
					   ((rowClicked + prerow) < this.numberOfRowsInMineField + 1 ) &&	
					   ((columnClicked + precolumn) > 0 ) &&  ((columnClicked + precolumn) < this.numberOfColumnsInMineField + 1 ) ){
				   rippleUncover(rowClicked + prerow, columnClicked + precolumn);
			   }
		   }
	   }
	   return ;   
   }
    
  //(确保第一次点击不生成雷)
  //计算出随机生成雷及非雷区的值
    public void setMines(int currentRow, int currentColumn){
    	
    		Random random = new Random();
    		int mineRow;
    		int mineColumn;
    		int index;
    		//此处随机数的处理有些奇怪,没有进行边界判断
    		for(index = 0; index < this.totalNumberOfMines; ++index){
    			mineRow = random.nextInt(this.numberOfRowsInMineField);
    			mineColumn = random.nextInt(this.numberOfColumnsInMineField);
    			
    			int row = mineRow % this.numberOfRowsInMineField + 1;
    			int colunm = mineColumn % this.numberOfColumnsInMineField + 1;
    			
    			if((row != currentRow) ||(colunm != currentColumn)){
    				if(blocks[row][colunm].hasMine()){
    					index--;
    				}
    				else{
    					blocks[row][colunm].plantMine();
    				}
    			}
    			else{
    				index--;
    			}
    		}
    		
    		int nearbyCount ;
    		for(int row = 0; row < this.numberOfRowsInMineField + 2 ; ++row )
    			for(int column = 0 ; column < this.numberOfColumnsInMineField + 2 ; ++column){
    				nearbyCount = 0;
    				//how to deal with if the block has mine itself??????????
    				//如果是雷区,仍进行计算,其实大可不必,该处可以改进
    				//判断,如果不是边界,进行计算(边界并不算在雷区内,只是为了方便雷区数字的计算)
    				if((row != 0)&&(row != numberOfRowsInMineField + 1)&&(column != 0)&&(column != numberOfColumnsInMineField + 1)){
    					for(int prerow = -1; prerow < 2; ++prerow){
    						for(int precolumn = -1; precolumn < 2; ++precolumn){
    							//if(pre)
    							if(blocks[row + prerow][column + precolumn].hasMine()){
    								nearbyCount++;
    							}
    						}
    					}	
    					blocks[row][column].setNumberOfMinesInSurrounding(nearbyCount);
    				}
    				//对边界的特殊处理,一开始就设其雷值为9(正常情况下雷最多有8个,不可能为9),然后将其打开~~
    				else{
    					blocks[row][column].setNumberOfMinesInSurrounding(9);
    					//blocks[row][column].openBlock();
    				}
    			}
    		//这里留一个未解决的问题,就是getnumberOfMineInSurrounding的值是不正确的,回来解决~~
    		//该问题已解决,是Block类中setNumberOfMinesInSurrounding与setNumberOfMineInSurrounding两函数
    		//名字太相似而导致的~~开始没有注意到二者的区别
    	}
    
    private void showMineField()
	{
		for(int row = 1; row < numberOfRowsInMineField + 1; row++){
			TableRow tableRow = new TableRow(this);
			//设置布局的宽度大小
			tableRow.setLayoutParams(new LayoutParams((blockDimension + 2 * blockPadding) * numberOfColumnsInMineField ,
													blockDimension + 2 * blockPadding));
			
			for(int column = 1; column < numberOfColumnsInMineField + 1; column++){
				
				blocks[row][column].setLayoutParams(new LayoutParams(blockDimension + 2 * blockPadding  ,
						blockDimension + 2 * blockPadding));
				blocks[row][column].setPadding(blockPadding, blockPadding, blockPadding, blockPadding);
				
				tableRow.addView(blocks[row][column]);	
			}	
			//addView方法中仍然需要声明其大小
			mineField.addView(tableRow, new TableLayout.LayoutParams((blockDimension + 2 * blockPadding) * numberOfColumnsInMineField , 
					blockDimension + 2 * blockPadding));
		}
	}
    
	//计时器
	private Runnable updateTimeElasped = new Runnable(){
		public void run(){
			long currentTimeMillis = System.currentTimeMillis();
			secondsPassed++;

			if (secondsPassed < 10)
			{
				txtTimer.setText("00" + Integer.toString(secondsPassed));
			}
			else if (secondsPassed < 100)
			{
				txtTimer.setText("0" + Integer.toString(secondsPassed));
			}
			else
			{
				txtTimer.setText(Integer.toString(secondsPassed));
			}
			
			//postAtTime() :Causes the Runnable r to be added to the message queue, 
			//to be run at a specific time given by uptimeMillis.
			timer.postAtTime(this, currentTimeMillis);//post this at this time
			
			//			postDelayed(Runnable r, long delayMillis) 
			//			Causes the Runnable r to be added to the message queue, 
			//to be run after the specified amount of time elapses.
			
			timer.postDelayed(updateTimeElasped, 1000);
		}
	};
	
	public void startTimer(){
		timer.removeCallbacks(updateTimeElasped);
		timer.postDelayed(updateTimeElasped, 1000);
	}
	
	public void stopTimer(){
		timer.removeCallbacks(updateTimeElasped);
	}
	private void showDialog(String message, int milliseconds, boolean useSmileImage, boolean useCoolImage)
	{
		// show message
		Toast dialog = Toast.makeText(
				getApplicationContext(),
				message,
				Toast.LENGTH_LONG);

		dialog.setGravity(Gravity.CENTER, 0, 0);
		LinearLayout dialogView = (LinearLayout) dialog.getView();
		ImageView coolImage = new ImageView(getApplicationContext());
		if (useSmileImage)
		{
			coolImage.setImageResource(R.drawable.smile);
		}
		else if (useCoolImage)
		{
			coolImage.setImageResource(R.drawable.cool);
		}
		else
		{
			coolImage.setImageResource(R.drawable.sad);
		}
		dialogView.addView(coolImage, 0);
		dialog.setDuration(milliseconds);
		dialog.show();
	}
};