2012-01-04
[Android] 15 パズル (2) - カスタム・ボタン
================================================================================
前回の「15 パズル (1) - テーブル・レイアウト」
http://blog.goo.ne.jp/marunomarunogoo/e/36e4574a26b3362719a348dcdaf003d3
は、16 個の数字ボタンすべてに別々のハンドラーを設定していた。
このサンプルは、上記のサンプルに対し、次のようにしている。
・数字ボタンに対するハンドラーをまとめた
・ボタン・クラスを継承したカスタム・ボタンを作る
・4x4 のボードに対応するクラスを作り、そこで、ボタンの移動などを管理する
今回作ったクラスはつぎの 3 つ。
FifteenPuzzleActivity アクティビティ
FifteenPuzzleBoard ボード
NumberButton 数字ブロック(カスタム・ボタン)
なお、前回も使ったつぎのクラスに変更はない。
ArrayCollectionUtil 配列やコレクション関係のユーティリティ
SymmetricGroupUtil 対称群と置換のユーティリティ
■ アクティビティ
ハンドラーを定義して、ボードのメソッドを呼び出すだけになった。
□ FifteenPuzzleActivity.java
---
package jp.marunomaruno.android.fifteenpuzzle;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
public class FifteenPuzzleActivity extends Activity {
private FifteenPuzzleBoard board; // 15パズルのボード // (1)
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
board = new FifteenPuzzleBoard(this); // (2)
}
/**
* 数字ボタン・クリック時のハンドラー
* @param v
*/
public void onClickNumberButton(View v) {
board.onClickNumberBlock((NumberButton) v); // (3)
}
/**
* 開始ボタン・クリック時のハンドラー
* @param v
*/
public void onClickStartButton(View v) {
board.reset(); // (4)
}
}
---
(1)(2) 15パズルのボード
4x4 のボードのオブジェクト。数字をずらすロジックなどは、こちらにある。
private FifteenPuzzleBoard board; // 15パズルのボード // (1)
board = new FifteenPuzzleBoard(this); // (2)
(3) 数字ボタン・クリック時のハンドラー呼び出し
board.onClickNumberBlock((NumberButton) v); // (3)
(4) 開始ボタン・クリック時のハンドラー呼び出し
board.reset(); // (4)
■ ボード
4x4 のボードを管理するクラス。数字ボタンをクリックしたときの動きなどを規定してい
る。
□ FifteenPuzzleBoard.java
---
package jp.marunomaruno.android.fifteenpuzzle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import jp.marunomaruno.android.util.ArrayCollectionUtil;
import jp.marunomaruno.android.util.SymmetricGroupUtil;
import android.app.Activity;
import android.content.Context;
public class FifteenPuzzleBoard {
public Context context;
public static final int ORDER = 4; // 4次の正方行列
private NumberButton[] numberButtons;
private int emptyBlockIndex = ORDER * ORDER;
public FifteenPuzzleBoard(Context context) {
this.context = context;
Activity activity = (Activity) context;
numberButtons = new NumberButton[] {
null, // ボード上の番号と合わせるため、インデックス0をダミーとする
(NumberButton) activity.findViewById(R.id.numberButton1),
(NumberButton) activity.findViewById(R.id.numberButton2),
(NumberButton) activity.findViewById(R.id.numberButton3),
(NumberButton) activity.findViewById(R.id.numberButton4),
(NumberButton) activity.findViewById(R.id.numberButton5),
(NumberButton) activity.findViewById(R.id.numberButton6),
(NumberButton) activity.findViewById(R.id.numberButton7),
(NumberButton) activity.findViewById(R.id.numberButton8),
(NumberButton) activity.findViewById(R.id.numberButton9),
(NumberButton) activity.findViewById(R.id.numberButton10),
(NumberButton) activity.findViewById(R.id.numberButton11),
(NumberButton) activity.findViewById(R.id.numberButton12),
(NumberButton) activity.findViewById(R.id.numberButton13),
(NumberButton) activity.findViewById(R.id.numberButton14),
(NumberButton) activity.findViewById(R.id.numberButton15),
(NumberButton) activity.findViewById(R.id.numberButton16),
};
// 最後のブロックを空とする
numberButtons[emptyBlockIndex].setText(R.string.emptyText);
log("initialize() ");
}
/**
* 空ブロックが指定されたインデックスに入っているかどうか判定する。
* @param indexes
* @return 空ブロックが指定されたインデックスに入っていればtrue
*/
private boolean isEmptyBlockIndexIn(int... indexes) {
assert indexes.length > 0;
return ArrayCollectionUtil.isKeyIn(emptyBlockIndex, indexes);
}
/**
* 数字ボタン・クリック時のハンドラー
* @param v
*/
public void onClickNumberBlock(NumberButton button) {
// このブロックに対して、水平方向に有効な空ブロックの位置
final int[][] HORIZONTAL_INDEXES = new int[][]{ // (1)
null,
{2, 3, 4},
{1, 3, 4},
{1, 2, 4},
{1, 2, 3},
{6, 7, 8},
{5, 7, 8},
{5, 6, 8},
{5, 6, 7},
{10, 11, 12},
{9, 11, 12},
{9, 10, 12},
{9, 10, 11},
{14, 15, 16},
{13, 15, 16},
{13, 14, 16},
{13, 14, 15},
};
// このブロックに対して、垂直方向に有効な空ブロックの位置
final int[][] VERTICAL_INDEXES = new int[][]{ // (2)
null,
{5, 9, 13},
{6, 10, 14},
{7, 11, 15},
{8, 12, 16},
{1, 9, 13},
{2, 10, 14},
{3, 11, 15},
{4, 12, 16},
{1, 5, 13},
{2, 6, 14},
{3, 7, 15},
{4, 8, 16},
{1, 5, 9},
{2, 6, 10},
{3, 7, 11},
{4, 8, 12},
};
System.out.printf("onClick() %s", button.toString());
// 空の場合、何もしない
if (isEmptyBlock(button)) {
System.out.println(": empty");
return;
}
int blockIndex = button.getBlockIndex();
if (isEmptyBlockIndexIn(HORIZONTAL_INDEXES[blockIndex])) { // (3)
rotateHorizontal(blockIndex); // (4)
System.out.println(); // 最後にlogを出すための改行
return;
}
if (isEmptyBlockIndexIn(VERTICAL_INDEXES[blockIndex])) {
rotateVertical(blockIndex); // (5)
System.out.println(); // 最後にlogを出すための改行
return;
}
System.out.println(); // 最後にlogを出すための改行
}
/**
* 垂直にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateVertical(int index) {
if (index < emptyBlockIndex) {
rotateDown(index);
} else {
rotateUp(index);
}
}
/**
* 水平にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateHorizontal(int index) {
if (index < emptyBlockIndex) {
rotateRight(index);
} else {
rotateLeft(index);
}
}
/**
* 左にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateLeft(int index) {
System.out.printf("rotateLeft() %d %d%n", index, emptyBlockIndex);
rotateAscending(index, 1);
}
/**
* 右にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateRight(int index) {
System.out.printf("rotateRight() %d %d%n", index, emptyBlockIndex);
rotateDescending(index, 1);
}
/**
* 下にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateDown(int index) {
System.out.printf("rotateDown() %d %d%n", index, emptyBlockIndex);
rotateDescending(index, ORDER);
}
/**
* 上にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateUp(int index) {
System.out.printf("rotateUp() %d %d%n", index, emptyBlockIndex);
rotateAscending(index, ORDER);
}
/**
* 降順にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateDescending(int index, int step) {
for (int i = emptyBlockIndex; i > index; i -= step) {
numberButtons[i].setText(numberButtons[i - step].getText());
}
numberButtons[index].setText(R.string.emptyText);
emptyBlockIndex = index;
}
/**
* 昇順にローテートする。
* @param index 空ブロックを置く位置
*/
private void rotateAscending(int index, int step) {
for (int i = emptyBlockIndex; i < index; i += step) {
numberButtons[i].setText(numberButtons[i + step].getText());
}
numberButtons[index].setText(R.string.emptyText);
emptyBlockIndex = index;
}
/**
* このブロックが空かどうかを判断する。
* @return 空の場合true
*/
public boolean isEmptyBlock(NumberButton button) {
return button.getBlockIndex() == emptyBlockIndex;
}
/**
* ボードをリセットする
*/
public void reset() {
// 1~15 までの乱数を生成する
List<Integer> numberList = new ArrayList<Integer>();
numberList.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15));
int[] numberArray;
do {
Collections.shuffle(numberList);
numberArray = ArrayCollectionUtil.toArray(numberList);
} while (SymmetricGroupUtil.sgn(numberArray) != 1); // 解けない問題(遇置換でない)なら作り直し
// 数字ブロックを設定する
for (int i = 1; i < numberArray.length + 1; i++) {
numberButtons[i].setText(String.valueOf(numberArray[i - 1]));
}
// 空ブロックを設定する
numberButtons[ORDER * ORDER].setText(R.string.emptyText);
emptyBlockIndex = ORDER * ORDER;
log("onClickStartButton() ");
}
private void log(String message) {
System.out.println(message);
System.out.print(toString());
}
@Override
public String toString() {
return String.format(
"[%s %s %s %s]%n[%s %s %s %s]%n[%s %s %s %s]%n[%s %s %s %s]%n",
numberButtons[1].getText(), numberButtons[2].getText(),
numberButtons[3].getText(), numberButtons[4].getText(),
numberButtons[5].getText(), numberButtons[6].getText(),
numberButtons[7].getText(), numberButtons[8].getText(),
numberButtons[7].getText(), numberButtons[10].getText(),
numberButtons[11].getText(), numberButtons[12].getText(),
numberButtons[13].getText(), numberButtons[14].getText(),
numberButtons[15].getText(), numberButtons[16].getText());
}
}
---
(1) このブロックに対して、水平方向に有効な空ブロックの位置の定義
最初はダミーで null を入れている。
final int[][] HORIZONTAL_INDEXES = new int[][]{ // (1)
null,
{2, 3, 4},
... (中略) ...
{13, 14, 15},
};
たとえば、自身が 2 のときは、水平方向に動かす場合、1 または 3, 4 に空ブロックがあることになる。
(2) このブロックに対して、垂直方向に有効な空ブロックの位置の定義
final int[][] VERTICAL_INDEXES = new int[][]{ // (2)
null,
{5, 9, 13},
... (中略) ...
{4, 8, 12},
};
(3)(4) 水平方向に空ブロックがあれば、水平方向にローテートする
if (isEmptyBlockIndexIn(HORIZONTAL_INDEXES[blockIndex])) { // (3)
rotateHorizontal(blockIndex); // (4)
(5) 垂直方向に空ブロックがあれば、垂直方向にローテートする
rotateVertical(blockIndex); // (5)
なお、水平方向、垂直方向のローテートは、自身と空ブロックの位置関係により、
右・左ローテート、上・下ローテートすることになる。
また、左・上のローテートは、空ブロックを番号の大きい方向に持っていく(昇順)ロー
テート、右・下のローテートは、空ブロックを番号の小さい方向に持っていく(降順)ロー
テートとなっているので、ロジックはこの昇順・降順ローテートが担当する。
■ 数字ブロック(ボタン)
Button クラスを継承したカスタム・ボタンのクラス。
現在のブロックの位置を示すインデックスを管理する。
□ NumberButton.java
---
package jp.marunomaruno.android.fifteenpuzzle;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.Button;
/**
* 数字ボタン。
* 2つの番号で管理する。
* ・ブロック・インデックス: 生成時のインデックスで、変更されない
* ・番号: ブロックに表示される番号。
* @author marunomaruno
*/
public class NumberButton extends Button { // (1)
/**
* 空ブロックを示す番号
*/
public static final int EMPTY_NUMBER = 0; // (2)
private static int maxNumber = 0; // 最大の番号 // (3)
private int blockIndex; // このブロックのインデックス(生成時のまま変更なし)// (4)
public NumberButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
public NumberButton(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public NumberButton(Context context) {
super(context);
initialize();
}
private void initialize() {
maxNumber++; // (5)
blockIndex = maxNumber;
setText(String.valueOf(blockIndex));
}
/**
* このブロックの番号を設定する。
* @param newNumber 新しい番号
*/
public void setNumber(int newNumber) {
if (newNumber != EMPTY_NUMBER) { // (6)
setText(String.valueOf(newNumber));
} else {
setText(getResources().getString(R.string.emptyText));
}
}
/**
* 番号を取得する。
* @return このブロックの番号。数値でない場合は0.
*/
public int getNumber() {
try {
return Integer.parseInt(getText().toString()); // (7)
} catch (NumberFormatException e) {
return EMPTY_NUMBER;
}
}
/**
* このブロックのインデックスを取得する。
* @return このブロックのインデックス
*/
public int getBlockIndex() {
return blockIndex;
}
@Override
public String toString() {
return String.format("NB[%d, %s]", blockIndex, getText());
}
}
---
(1) Button クラスを継承
public class NumberButton extends Button { // (1)
(2) 空ブロックを示す番号
public static final int EMPTY_NUMBER = 0; // (2)
(3)(5) 最大の番号
このインスタンスを生成するときにインデックスとして番号を振っていくので、現在の最
大の番号を静的に保持する。
private static int maxNumber = 0; // 最大の番号 // (3)
maxNumber++; // (5)
(4) このブロックのインデックス
このブロックのインデックス。これは、生成時のまま変更しない。
private int blockIndex; // このブロックのインデックス(生成時のまま変更な
し)// (4)
(6) このブロックの番号を設定
空ブロックでなければ、引数の値を設定する。
空ブロックのときは、リソースで指定された空ブロックの記号を設定する。
if (newNumber != EMPTY_NUMBER) { // (6)
setText(String.valueOf(newNumber));
} else {
setText(getResources().getString(R.string.emptyText));
}
(7) このブロックの番号を取得
数値でない場合は 0 にする。
try {
return Integer.parseInt(getText().toString()); // (7)
} catch (NumberFormatException e) {
return EMPTY_NUMBER;
}
■レイアウト
カスタム・ボタンを使っているので、ボタンのクラス名は、完全修飾名としてつぎを指定
する。
jp.marunomaruno.android.fifteenpuzzle.NumberButton
数字ブロッククリック時のハンドラーを同じものにした。
□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="vertical" >
<Button
android:id="@+id/start_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="onClickStartButton"
android:text="@string/start" />
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" >
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton1"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="1"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton2"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="2"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton3"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="3"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton4"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="4"
android:textAppearance="?android:attr/textAppearanceLarge" />
</TableRow>
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton5"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="5"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton6"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="6"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton7"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="7"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton8"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="8"
android:textAppearance="?android:attr/textAppearanceLarge" />
</TableRow>
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton9"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="9"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton10"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="10"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton11"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="11"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton12"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="12"
android:textAppearance="?android:attr/textAppearanceLarge" />
</TableRow>
<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton13"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="13"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton14"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="14"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton15"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="15"
android:textAppearance="?android:attr/textAppearanceLarge" />
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton16"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="@string/emptyText"
android:textAppearance="?android:attr/textAppearanceLarge" />
</TableRow>
</TableLayout>
</LinearLayout>
---
(1) カスタム・ボタンの指定
<jp.marunomaruno.android.fifteenpuzzle.NumberButton
android:id="@+id/numberButton1"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="90px"
android:layout_margin="1px"
android:onClick="onClickNumberButton"
android:text="1"
android:textAppearance="?android:attr/textAppearanceLarge" />
以上