參考資料: Amazon.com: Hello, Android: Introducing Google's Mobile Development Platform (Pragmatic Programmers) (9781934356562): Ed Burnette: Books
這段內容主要為:
第4章 2D繪圖
4. 處理使用者輸入
4.4 剩下還未處理部分
4.4.1 建立銀幕小鍵盤(Keypad)
4.4.2 實作遊戲程式內部邏輯
4.4 剩下還未處理部分
在4.3中有一個部分: 銀幕小鍵盤(Keypad)尚未處理, 這部份程式繪圖無關, 不需要這部份程式也可以運作, 但可以讓介面輸入更人性化.
4.4.1 建立銀幕小鍵盤(Keypad)
有一些智慧型手機沒有鍵盤, 所以會在銀幕上顯示一個小鍵盤讓使用者輸入, 這小鍵盤會以一個九宮格的方式顯示1~9的數字, 使用者按完數字後就回到程式本身. 要顯示這樣的小鍵盤在我們設計的數獨中, 先在res/layout/底下加入一個keypad.xml使用者介面:
## res/layout/keypad.xml
<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keypad"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="*" >
<TableRow>
<Button android:id="@+id/keypad_1" android:text="1" ></Button>
<Button android:id="@+id/keypad_2" android:text="2" ></Button>
<Button android:id="@+id/keypad_3" android:text="3" ></Button>
</TableRow>
<TableRow>
<Button android:id="@+id/keypad_4" android:text="4" ></Button>
<Button android:id="@+id/keypad_5" android:text="5" ></Button>
<Button android:id="@+id/keypad_6" android:text="6" ></Button>
</TableRow>
<TableRow>
<Button android:id="@+id/keypad_7" android:text="7" ></Button>
<Button android:id="@+id/keypad_8" android:text="8" ></Button>
<Button android:id="@+id/keypad_9" android:text="9" ></Button>
</TableRow>
</TableLayout>
接下來定義一個Keypad類別:
## src/org/example/sudoku/Keypad.java
package org.example.sudoku;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
public class Keypad extends Dialog {
protected static final String TAG = "Sudoku" ;
private final View keys[] = new View[9];
private View keypad;
private final int useds[];
private final PuzzleView puzzleView;
public Keypad(Context context, int useds[], PuzzleView puzzleView) {
super(context);
this.useds = useds;
this.puzzleView = puzzleView;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(R.string.keypad_title);
setContentView(R.layout.keypad);
findViews();
for (int element : useds) {
if (element != 0)
keys[element - 1].setVisibility(View.INVISIBLE);
}
setListeners();
}
// ...
}
呼叫Keypad的程式可以把已經出現過在格子內或行列上的號碼放在useds[], onCreate()會判斷中假如某個號碼已經出現過了, 就可以把該號碼隱藏(利用View的setVisibility()), 輔助使用者輸入錯誤的號碼.
findViews( ) 主要是將定義在keypad.xml中的按鈕取出放在keys[]中, 之後會顯示在銀幕上變成一個小鍵盤:
## src/org/example/sudoku/Keypad.java
private void findViews() {
keypad = findViewById(R.id.keypad);
keys[0] = findViewById(R.id.keypad_1);
keys[1] = findViewById(R.id.keypad_2);
keys[2] = findViewById(R.id.keypad_3);
keys[3] = findViewById(R.id.keypad_4);
keys[4] = findViewById(R.id.keypad_5);
keys[5] = findViewById(R.id.keypad_6);
keys[6] = findViewById(R.id.keypad_7);
keys[7] = findViewById(R.id.keypad_8);
keys[8] = findViewById(R.id.keypad_9);
}
在Keypad中有一個setListeners( ), 主要是用來設定每個keypad按下按鍵後會執行的動作, 這邊用一個迴圈設定按下後會呼叫retuenResult(t)來設定玩家選到的那格, 填入對應的數字, 預設玩家選到的那沒有按鈕的地方, 會把該格的數字給清空:
## src/org/example/sudoku/Keypad.java
private void setListeners() {
for (int i = 0; i < keys.length; i++) {
final int t = i + 1;
keys[i].setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
returnResult(t);
}});
}
// 回傳0, 清除數字
keypad.setOnClickListener(new View.OnClickListener(){
public void onClick(View v) {
returnResult(0);
}});
}
另外也可以考慮將onKeyDown( )加入, 這樣使用者用鍵盤輸入的話也可以運作:
## src/org/example/sudoku/Keypad.java
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
int tile = 0;
switch (keyCode) {
case KeyEvent.KEYCODE_0:
case KeyEvent.KEYCODE_SPACE: tile = 0; break;
case KeyEvent.KEYCODE_1: tile = 1; break;
case KeyEvent.KEYCODE_2: tile = 2; break;
case KeyEvent.KEYCODE_3: tile = 3; break;
case KeyEvent.KEYCODE_4: tile = 4; break;
case KeyEvent.KEYCODE_5: tile = 5; break;
case KeyEvent.KEYCODE_6: tile = 6; break;
case KeyEvent.KEYCODE_7: tile = 7; break;
case KeyEvent.KEYCODE_8: tile = 8; break;
case KeyEvent.KEYCODE_9: tile = 9; break;
default:
return super.onKeyDown(keyCode, event);
}
if (isValid(tile)) {
returnResult(tile);
}
return true;
}
用isValid()來判斷輸入的數字是否合法, 假如符合的話就將數字設定進該格, 否則就直接忽略玩家所輸入的數字.
接下來補齊isValid()與returnResult(), isValid()檢查傳進來的數字是否合法(判斷此數字是不是在九宮格中, 或行列中被用過, 這邊假設已經判斷好把使用過的數字放在useds[], 所以程式只需檢查useds[], useds[]的處理與檢查, 會在下一節中提到):
## src/org/example/sudoku/Keypad.java
private boolean isValid(int tile) {
for (int t : useds) {
if (tile == t)
return false;
}
return true;
}
一般標準的for寫法是:for(初始變數; 判斷式; 遞增式), 這邊的for (int t : useds)是一個進階的寫法, 可以參考 http://download.oracle.com/javase/tutorial/java/nutsandbolts/for.html, 這種寫法可以支援Collections與array, 假如不這麼寫的話, 完整的寫法是:
for ( int t=0; t<useds.length ; t++)
ps. for的syntax
for (initialization; termination; increment) {
statement(s)
}
再來補齊returnResult(), 用來設定選到的該格的數字:
## src/org/example/sudoku/Keypad.java
private void returnResult(int tile) {
puzzleView.setSelectedTile(tile);
dismiss();
}
上面的用puzzleView來設定該格子顯示對應的數字, 而dismiss()可以參考: http://developer.android.com/reference/android/app/Dialog.html 代表將Keypad對話盒給關閉.
至此, Keypad類別已經完成, 在之前PuzzleView中輸入時有設定, 假如使用者按下某個方格, 就呼叫game.showKeypadOrError來將Keypad顯示出來, 假如該格子已無可輸入的數字, 則用Toast類別(http://developer.android.com/reference/android/widget/Toast.html)顯示一個警告字(No Moves)讓玩家知道, 其實會這樣通常代表有數字輸入錯誤, 造成該格沒東西可以輸入:
## src/org/example/sudoku/Game.java
protected void showKeypadOrError(int x, int y) {
int tiles[] = getUsedTiles(x, y);
if (tiles.length == 9) {
Toast toast = Toast.makeText(this, R.string.no_moves_label, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
} else {
Log.d(TAG, "showKeypad: used=" + toPuzzleString(tiles));
Dialog v = new Keypad(this, tiles, puzzleView);
v.show();
}
}
目前大致上ok, 除了判斷useds[], 執行結果會像下圖:
4.4.2 實作遊戲程式內部邏輯
Game.java主要在處理遊戲的一些判斷式, 最重要的是以數獨的規則, 來判斷還有哪一些數字可以或不可以輸入. 底下setTileIfValid()是一個很關鍵的方法, 傳入的參數為x, y座標, 與一個數字, setTileValid()會根據數獨的規則判斷該格是否可以輸入該數字, 假如可以的話將數字填入, 並回傳true, 假如不行便回傳false:
## src/org/example/sudoku/Game.java
protected boolean setTileIfValid(int x, int y, int value) {
int tiles[] = getUsedTiles(x, y);
if (value != 0) {
for (int tile : tiles) {
if (tile == value)
return false;
}
}
setTile(x, y, value);
calculateUsedTiles();
return true;
}
要偵測每一格是不是被使用過了, 首先建立一個三維陣列, 紀錄該格有哪一些數字已經被用過了(紀錄該格在其九宮格與行列中"已經看到"的數字), 也建立一個getUsedTiles()來傳回該格已經被用過數字的陣列:
## src/org/example/sudoku/Game.java
private final int used[][][] = new int[9][9][];
protected int[] getUsedTiles(int x, int y) {
return used[x][y];
}
重新計算這個三維陣列非常的費工(要一個個去比對其九宮格與行列, 見下面的程式即可理解), 所以只有在有必要的時候再整個重新計算, 底下是重新計算的方法 calculateUsedTiles():
## src/org/example/sudoku/Game.java
private void calculateUsedTiles() {
for (int x = 0; x < 9; x++) {
for (int y = 0; y < 9; y++) {
used[x][y] = calculateUsedTiles(x, y);
// Log.d(TAG, "used[" + x + "][" + y + "] = "
// + toPuzzleString(used[x][y]));
}
}
}
calculateUsedTiles( )將一個個格子9x9拿出來, 呼叫calculateUsedTiles(x, y)比對其九宮格與行列:
## src/org/example/sudoku/Game.java
private int[] calculateUsedTiles(int x, int y) {
int c[] = new int[9];
// horizontal 判斷x軸, 橫列
for (int i = 0; i < 9; i++) {
// 代表自己不用判斷
if (i == y)
continue;
int t = getTile(x, i);
if (t != 0)
c[t - 1] = t;
}
// vertical 判斷y軸, 直行
for (int i = 0; i < 9; i++) {
// 代表自己不用判斷
if (i == x)
continue;
int t = getTile(i, y);
if (t != 0)
c[t - 1] = t;
}
// same cell block 判斷九宮格中
int startx = (x / 3) * 3;
int starty = (y / 3) * 3;
for (int i = startx; i < startx + 3; i++) {
for (int j = starty; j < starty + 3; j++) {
// 代表自己不用判斷
if (i == x && j == y)
continue;
int t = getTile(i, j);
if (t != 0)
c[t - 1] = t;
}
}
// compress 最後統整, 將c[]中空的移除
int nused = 0;
for (int t : c) {
if (t != 0)
nused++;
}
int c1[] = new int[nused];
nused = 0;
for (int t : c) {
if (t != 0)
c1[nused++] = t;
}
return c1;
}
private int[] calculateUsedTiles(int x, int y) {
int c[] = new int[9];
// horizontal 判斷x軸, 橫列
for (int i = 0; i < 9; i++) {
// 代表自己不用判斷
if (i == y)
continue;
int t = getTile(x, i);
if (t != 0)
c[t - 1] = t;
}
// vertical 判斷y軸, 直行
for (int i = 0; i < 9; i++) {
// 代表自己不用判斷
if (i == x)
continue;
int t = getTile(i, y);
if (t != 0)
c[t - 1] = t;
}
// same cell block 判斷九宮格中
int startx = (x / 3) * 3;
int starty = (y / 3) * 3;
for (int i = startx; i < startx + 3; i++) {
for (int j = starty; j < starty + 3; j++) {
// 代表自己不用判斷
if (i == x && j == y)
continue;
int t = getTile(i, j);
if (t != 0)
c[t - 1] = t;
}
}
// compress 最後統整, 將c[]中空的移除
int nused = 0;
for (int t : c) {
if (t != 0)
nused++;
}
int c1[] = new int[nused];
nused = 0;
for (int t : c) {
if (t != 0)
c1[nused++] = t;
}
return c1;
}
上面依次判斷直行, 橫列, 與九宮格內的數字是否出現, 有的話, 就寫入對應的c[], 比如有出現7, 就寫入c[6]=7. 直行橫列的比對較為可理解, 中間九宮格則先計算最左上角的起始x,y座標, 比如點到(4,5), 要計算該格最左上角的座標, 因為均為整數運算, xy先除以3得(1,1), 再乘以3得(3,3)即為最左上角座標. 最後compress把在c[0]中內容為0的排除, 這樣在利用array.length則可把馬上知道會有多少已知的數字被用過了.
沒有留言:
張貼留言