編輯:Android開發實例
文接上回,之前介紹了項目的架構,進行了功能的分析,同時進行了BoardView類及時間控制類的開發及幾個幾口的介紹。這次我們將完整的實現游戲棋盤的繪制與touch事件的處理,以及游戲核心算法中連接算法、hint自動幫助算法與判斷是否無解算法的實現。這些代碼的處理都在繼承自BoardView類的GameView類中。
首先在GameView類中添加實現本游戲主要算法的代碼,即連接算法的代碼(用於判斷給定的兩個位置的圖標能夠相連通):
/** * 本游戲的核心算法,判斷兩個連接點是否能夠連接,這裡傳進來的就是我們點擊的兩個點轉化成index的值 * @param p1 * @param p2 */ List<Point> p1Expand = new ArrayList<Point>(); List<Point> p2Expand = new ArrayList<Point>(); public boolean link(Point p1,Point p2){ if(p1.equals(p2)){ return false; } path.clear(); if(map[p1.x][p1.y] == map[p2.x][p2.y]){ if(linkDirect(p1,p2)){ path.add(p1); path.add(p2); return true; } /** * 一個拐點的判斷 */ Point px = new Point(p1.x,p2.y); //假設第一種可能點 if(map[p1.x][p2.y] == 0 && linkDirect(p1,px) && linkDirect(px,p2)){ path.add(p1); path.add(px); path.add(p2); return true; } Point py = new Point(p2.x,p1.y); //假設第二種可能點 if(map[p2.x][p1.y] == 0 && linkDirect(p1,py) && linkDirect(py,p2)){//首先判斷map[p2.x][p1.y]中介點是否有圖標 path.add(p1); path.add(py); path.add(p2); return true; } /** * 兩個折點(corner) */ expandX(p1,p1Expand); expandX(p2,p2Expand); for(int i = 0; i < p1Expand.size(); i++) for(int j = 0; j < p2Expand.size(); j++){ if(p1Expand.get(i).x == p2Expand.get(j).x){ if(linkDirect(p1Expand.get(i),p2Expand.get(j))){ path.add(p1); path.add(p1Expand.get(i)); path.add(p2Expand.get(j)); path.add(p2); return true; } } } expandY(p1,p1Expand); expandY(p2,p2Expand); for(Point exp1:p1Expand) for(Point exp2:p2Expand){ if(exp1.y == exp2.y){ if(linkDirect(exp1,exp2)){ path.add(p1); path.add(exp1); path.add(exp2); path.add(p2); return true; } } } return false; //最後三種方式都不能連通,還是要return false ,不然在兩個同樣的圖標下卻沒有返回值! } return false; } /** * 判斷直線鏈接,無拐角,傳進來的點值是ScreenToIndex過的了,不過這裡傳進來的不一定就是我們點擊的點,也可能是我們的拐角點(輔助點) * @param p1 * @param p2 */ public boolean linkDirect(Point p1,Point p2){ //if(map[p1.x][p1.y] == map[p2.x][p2.y]){ //縱向直線 if(p1.x == p2.x){ int y1 = Math.min(p1.y, p2.y); int y2 = Math.max(p1.y, p2.y); boolean flag = true; for(int y = y1 + 1; y < y2; y++){//這個循環裡容易漏掉兩個相鄰的情況,所以才加上上面的flag樣式 if(map[p1.x][y] != 0){ flag = false; break; } } if(flag){ return true; } } //橫直線判斷 if(p1.y == p2.y){ int x1 = Math.min(p1.x, p2.x); int x2 = Math.max(p1.x, p2.x); boolean flag = true; for(int x = x1 + 1; x < x2; x++){ if(map[x][p1.y] != 0){ flag = false; break; } } if(flag){ return true; } } //} return false; } /** * 向x方向擴展,傳進來的點是index過的 * @param p * @param list */ public void expandX(Point p,List<Point> list){ list.clear(); for(int x = p.x + 1; x < xCount; x++){//注意此時可以等於xCount -1了 if(map[x][p.y] != 0) break; list.add(new Point(x,p.y)); } for(int x = p.x -1; x >= 0; x--){ if(map[x][p.y] != 0) break; list.add(new Point(x,p.y)); } } /** * 向Y方向擴展,傳進來的點是index過的,而list是作為“返回值”需要保存的值 * @param p * @param list */ public void expandY(Point p,List<Point> list){ list.clear(); for(int y = p.y + 1; y < yCount; y ++){ if(map[p.x][y] != 0) break; list.add(new Point(p.x,y)); } for(int y = p.y -1 ; y >= 0; y--){ if(map[p.x][y] != 0) break; list.add(new Point(p.x,y)); } }
代碼中盡量添加注釋,此段代碼中實現了第一篇文章中進行的算法分析,其中link(Point p1,Point p2)函數作為算法真正的完整實現者,算法的主邏輯有它實現,
linkDirect(Point p1,Point p2)函數作為一個工具函數,用於判斷給定的兩個位置(注意不是兩個圖標,因為給定的位置不一定含有圖標,當我們在判斷
”一折型“和“二折型”的情況的時候即使如此)。而expandX(Point p,List<Point> list)與 expandY(Point p,List<Point> list)兩個方法的同樣作為工具函數,
在判斷“二折型”情況時候將會使用,也就是前面所說的“橫向掃描”與“縱橫掃描”。而對於link(Point p1,Point p2)函數中,我們的邏輯還是將大問題化為小問題處理。
最終還是分解到調用linkDirect(Point p1,Point p2)函數來進行“直線型”的處理。
以上即是程序的連接算法的實現,除了程序算法邏輯的理解之外,還需注意在判斷的時候,若能夠連通,我們已經將
private List<Point> path = new ArrayList<Point>();保存連通路徑的path附上值,記得當link函數返回true時,path中即保存了一條相通的路徑!完成了
連接算法,下一步我們將依賴於連接算法的實現,完成掃描是否當前地圖已經出現無解的情況,因為程序的地圖是隨機生成的,難免有時候會出現無解的情況;
下面我們將實現判斷是否處於無解狀態,實現函數:
/** *用於判斷是否當前已經無解 */ public boolean die(){ for(int y= 1; y < yCount; y++) //表示從此行中的一個元素開始掃描(起點) for(int x = 1; x < xCount; x++){ //表示此行中指定列,組成掃描起點 if(map[x][y] != 0){ for(int j = y; j < yCount; j++){//表示正在被掃描的行 if(j == y){//循環中的第一次掃描,為什麼特殊?因為此時不一定從一行中的第一個元素開始掃描 for(int i = x + 1; i < xCount - 1; i++){ if(map[x][y] == map[i][j] && link(new Point(x,y),new Point(i,j))){ return false; } } }else{ for(int i = 1; i < xCount -1; i++){ if(map[x][y] == map[i][j] && link(new Point(x,y),new Point(i,j))) return false; } } } } } return true; }
代碼中也有相應注釋,每一次判斷相當於一次遍歷棋盤,同時注意,如果die()函數返回為false,這則證明link()函數返回了true!前面已經提醒過:當link返回true時,我們用於保存連通路徑的path對象中已經保存了一條連通路徑的點的集合,只不過在die()函數中運行得到的是按遍歷順序而來的,並不是我們所指定的兩個始點與終點兩個圖標;所以在這兒,可以借die()的判斷,完成我們算法實現的第三個功能,即hint的自動幫助!
/** * 當點擊help按鈕時候調用,會幫助玩家消除一對圖標 */ public void autoHelp(){ if(help == 0){ //soundPlay.play(ID_SOUND_ERROR, 0); return ; }else{ //soundPlay.play(ID_SOUND_TIP, 0); help--; toolsChangedListener.onTipChanged(help); drawLine(path.toArray(new Point[] {})); refreshHandler.sendRefresh(500); } }
當然此處需要介紹一下最後一行代碼的來歷:
class RefreshHandler extends Handler{ @Override public void handleMessage(Message msg) { super.handleMessage(msg); if(msg.what == REFRESH_VIEW){ GameView.this.invalidate(); if(win()){ setMode(WIN); isStop = true; isContinue = false; }else if(die()){ //調用一次die方法!此時如果die返回為false,即還能夠連通 change(); //由於die中使用link方法檢測,所以此時path中的值又添加了進去, } //這對於我們使用autoHelp方法提供便利!!! } } /** * * @param delayTime */ public void sendRefresh(int delayTime){ Message msg = new Message(); this.removeMessages(0); msg.what = REFRESH_VIEW; this.sendMessageDelayed(msg, delayTime); } }
當然對於是否已經為贏了的判斷win()函數比較簡單,就是掃描棋盤,如果所有位置map值都為了0,即贏了,若不是,還未完成;這裡就不貼代碼了。
GameView類中還有一個職能就是初始化一張棋盤:
/** * 初始化地圖 */ public void initMap(){ int x = 1; int y = 0; for(int i = 1; i < xCount -1; i++) for(int j =1; j < yCount -1; j++){ map[i][j] = x; if(y == 1){ x ++; y = 0; if(x == iconCounts){ x = 1; } }else{ y = 1; } } change(); GameView.this.invalidate(); }
我們初始化棋盤時,利用前面講解的初始算法技術,遍歷棋盤,先將棋盤填滿,但是填滿首先還有一個規則就是每一種圖標的填入必須同時填入兩張,是為每種圖標都為偶數個而設定!介紹一下最後調用的change()函數,也是出自於第一篇的棋盤初始算法,用於隨機將棋盤中的圖標打亂:
/** * 隨機將現有的布局打亂,重新布局,map中現有圖標數量不變,相當於一次refresh */ public void change(){ Random random = new Random(); int tmp,xtmp,ytmp; for(int x = 1;x < xCount -1; x++){ for(int y = 1; y < yCount -1; y++){ xtmp = 1 + random.nextInt(xCount -2); ytmp = 1 + random.nextInt(yCount - 2); tmp = map[x][y]; map[x][y] = map[xtmp][ytmp]; map[xtmp][ytmp] = tmp; } } if(die()){ //如出現無解情況,即需要再次隨機重新打亂 change(); } }
GameView類還是一個View,在此類中我們還要重寫View的onTouchEvent方法:
/** * 對於選擇的處理,如果是第一次按下,則將其加入到selected當中, * 若是第二次(selected.size()==1),則先判斷能不能連通 */ @Override public boolean onTouchEvent(MotionEvent event) { int sx = (int)event.getX(); int sy = (int)event.getY(); Point p = screenToIndex(sx, sy); if(map[p.x][p.y] != 0){ if(selected.size() == 1){ if(link(selected.get(0),p)){ //能夠連通,path中的數據是在link判斷時如果返回真,方法內部就已經將數據添加進去 selected.add(p); drawLine(path.toArray(new Point[]{})); refreshHandler.sendRefresh(500); }else{ //不能夠連通 selected.clear(); selected.add(p); GameView.this.invalidate(); //在這兒說一下refreshHanler.sendRefresh(int) 跟單純調用GameView.this.invalidate()區別 //前者除了後者只擁有的刷新顯示之外,還加了是否已經無解跟是否已經完成任務的判斷的操作。 } }else{//此時的selected中的size只能等於0 selected.add(p); GameView.this.invalidate(); } } return super.onTouchEvent(event); }
方法中用到的selected是BoardView中的protected List<Point> selected = new ArrayList<Point>();代碼中對於功能及實現有相應的注釋。
到此我們可以提供接口startGame以供在程序的activity中調用:
public void startPlay(){ help = 3; refresh = 3; isContinue = true; isStop = false; toolsChangedListener.onRefreshChanged(refresh); toolsChangedListener.onTipChanged(help); leftTime = totalTime; initMap(); refreshTime = new RefreshTime(); Thread t = new Thread(refreshTime); //注意正確啟動一個實現Runnable接口的線程類 t.start(); GameView.this.invalidate(); }
注意GameView中並沒有實現相關的自定義的接口,而是我們將會在程序的activity中實現項目中涉及的三個接口,但是,我們可以在GameView中進行注冊:
public void setOnTimerListener(OnTimerListener onTimerListener){ this.timerListener = onTimerListener; } public void setOnToolsChangedListener(OnToolsChangeListener toolsChangeListener){ this.toolsChangedListener = toolsChangeListener; } public void setOnStateChangeListener(OnStateListener stateListener){ this.stateListener = stateListener; }
然後在程序的activity中調用GameView的相關函數進行初始化注冊。這樣,根據多態性的原理,在GameView當中調用的相關接口中的函數,也就是activity中實現的接口中的函數。這也是android程序中interface實現與注冊的一種方式。
以上已經基本描述了GameView的功能與最主要的實現。總結一下,實現了map的初始化,重寫了touch時間的處理函數,完成了程序的連接算法,hint自動幫助算法,die的無解判斷算法,還有用於更新顯示的繼承自Handler的內部類的實現。整個項目也已經基本成型了。
重申一下:之所以寫本系列的文章,為了記錄android小項目的經歷,增加實戰的能力,做個總結。並不是為了做出多麼新穎的項目,當然也是向不少的網友學習了的!
可以顯示在的Android任務,通過加載進度條的進展。進度條有兩種形狀。加載欄和加載微調(spinner)。在本章中,我們將討論微調(spinner)。Spinner 用
Android提供了許多方法來控制播放的音頻/視頻文件和流。其中該方法是通過一類稱為MediaPlayer。Android是提供MediaPlayer類訪問內置的媒體播放
系統啟動過程圖: Framework層所有的Service都是運行在SystemServer進程中;SystemServer進程是由Zygote進程創
這是個很簡單的問題,但每次隔一段時間後使用起來總是會出點亂子。這裡記錄下Logcat的步驟:1,在Activity裡申明tag變量(名字其實是隨便的,如下:pri