源码参考文档:
开始以为直接看源码很简单,大概思考了过程,就参考源码自己写程序,但中间出现了很多问题。终究是“纸上得来终觉浅,绝知此事要躬行”。以后一定不能手懒,要多动手。
源码分析:
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);
}
第一个为设置周围雷个数的函数,第二个是对方格进行背景变色即显示数字的设置,二者太相似了,极容易出错。在函数命名方面也需要注意。
主函数:
主要分为方格区显示、分布雷、为方格添加单击、长击监听事件、检验游戏是否成功或者失败、定时器显示、所需找到雷的个数显示等部分。如果哪位想自己动手写一下改程序,建议分模块逐步完善。否则后期代码的调试会非常麻烦。这里把写代码中遇到的错误贴出来:
在方块显示中,开始显示一直
不完全,如左上所示:
后来发现问题在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();
}
};