網頁

2011年1月9日 星期日

Android: 讀書心得#8 2D繪圖實例 (建立銀幕小鍵盤與遊戲程式內部邏輯)

接續上篇: 花栗鼠柑仔店: Android筆記: 讀書心得#7

參考資料: 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;
}

上面依次判斷直行, 橫列, 與九宮格內的數字是否出現, 有的話, 就寫入對應的c[], 比如有出現7, 就寫入c[6]=7. 直行橫列的比對較為可理解, 中間九宮格則先計算最左上角的起始x,y座標, 比如點到(4,5), 要計算該格最左上角的座標, 因為均為整數運算, xy先除以3得(1,1), 再乘以3得(3,3)即為最左上角座標. 最後compress把在c[0]中內容為0的排除, 這樣在利用array.length則可把馬上知道會有多少已知的數字被用過了. 



沒有留言:

張貼留言