Android Studioサンプルアプリ解説集 - 箱入り娘ゲームBoxGirl

イラストでよくわかるAndroidアプリのつくり方—Android Studio対応版

「箱入り娘」は古くからあるパズルで、大きさの違ういくつかのピースを動かし、「娘」と書かれたピースを外に出すゲームです。とりあえず、遊んでみてください。「執事」や「父」「母」などが邪魔をして、なかなか娘を外に出すことができません(必勝法はあるようですが)。
BoxGirlの実行例
![]() | → | ![]() |
グレーの部分が空きです。空きに接しているピースをクリックすると、ピースが空きの場所に動きます。上と右など2方向に空きがある場合は、ピースの上のほうをクリックすると上に移動し、右のほうをクリックすると右に移動します。
アプリは以下のような設定で作成してあります。表にない項目については、設定を変更していません。
項目名 | 設定する内容 |
---|---|
Application Name | BoxGirl |
Company Domain | sample.example.com |
このアプリでは、Javaでウィジェットを配置するので、あらかじめ配置されているTextViewウィジェットは削除してあります。
Javaのプログラムはアクティビティを表示するためのMainActivity.javaファイルと、ピースを表すクラスを記述したPiece.javaファイルに分かれています。
Javaプログラムの新しいファイルを追加するには、[プロジェクトウィンドウ]で[app]−[java]を開き、[com.example.sample.boxgirl](MainActivityのあるほう)を右クリックして[New]−[Java Class]を選びます。[Name:]に「Piece」と入力すると、Piece.javaファイルが作られます。
Java > java/com.example.sample.boxgirl/Piece.java
1: package com.example.sample.boxgirl; : 17: 18: public class Piece extends TextView { …… (1) 19: private String pieceName; // 名前(父とか母とか) 20: private int pieceType; // ピースのタイプ(父とか母とか) 21: private int pieceWidth; // ピースの幅 22: private int pieceHeight; // ピースの高さ 23: private int posX; // ピースの位置 24: private int posY; // ピースの位置 25: private int imgId; // 画像の種類 26: LayoutParams lp; 27: DisplayMetrics metrics = getResources().getDisplayMetrics(); … 画面の解像度の比率(dp/pixel)を求める 28: private int gridSize = (int) (metrics.density * 64); // 1グリッドの幅と高さ …… 幅1高さ1のピースのサイズ 29: 30: public Piece(Context c, int type, int x, int y) { …… (2) 31: super(c); 32: // typeによって変わる値 33: pieceType = type; 34: switch (pieceType) { 35: case 0: 36: pieceName = ""; 37: pieceWidth = 1; 38: pieceHeight = 1; 39: imgId = 0; // 画像なし 40: break; 41: case 1: 42: pieceName = "父"; ……「父」は幅が1、高さが2のピース。以下同様。 43: pieceWidth = 1; 44: pieceHeight = 2; 45: imgId = R.drawable.father; 46: break; 47: case 2: 48: pieceName = "母"; 49: pieceWidth = 1; 50: pieceHeight = 2; 51: imgId = R.drawable.mother; 52: break; 53: case 3: : (同様にして、タイプをもとに各ピースの設定を変える) : 99: case 6: 98: pieceName = "娘"; 99: pieceWidth = 2; 100: pieceHeight = 2; 101: imgId = R.drawable.daughter; 102: break; 103: } 104: // 初期位置 105: posX = x; 106: posY = y; 107: // コンポーネントの表示サイズ 108: this.setLayoutParams(new LayoutParams(pieceWidth * gridSize, pieceHeight * gridSize)); 109: // コンポーネントの位置を無理矢理決める 110: lp = (LayoutParams) this.getLayoutParams(); 111: lp.leftMargin = x * gridSize; 112: lp.topMargin = y * gridSize; 113: this.setLayoutParams(lp); 114: // 名前 115: this.setText(pieceName); 116: this.setBackgroundResource(imgId); 117: // 共通のパラメータ 118: this.setGravity(Gravity.RIGHT | Gravity.BOTTOM); 119: this.setTextSize(12); 120: this.setTextColor(Color.BLACK); 121: if (pieceType == 0) { 122: this.setBackgroundColor(Color.GRAY); 123: } else { 124: //this.setBackgroundColor(Color.rgb(255, 153, 0)); 125: } 126: this.setVisibility(View.VISIBLE); 127: } 128: 129: // メソッド…… (3) 130: // 位置を返す 131: public int getXPos() { 132: return posX; 133: } 134: 135: public int getYPos() { 136: return posY; 137: } 138: 139: // 位置をセットする 140: public void setXPos(int x) { 141: posX = x; 142: int y = posY; 143: lp.leftMargin = x * gridSize; 144: lp.topMargin = y * gridSize; 145: this.setLayoutParams(lp); 146: this.invalidate(); 147: } 148: 149: public void setYPos(int y) { 150: posY = y; 151: int x = posX; 152: lp.leftMargin = x * gridSize; 153: lp.topMargin = y * gridSize; 154: this.setLayoutParams(lp); 155: this.invalidate(); 156: } 157: 158: // 幅と高さをグリッド単位で返す 159: public int getWidthByGrid() { 160: return pieceWidth; 161: } 162: 163: public int getHeightByGrid() { 164: return pieceHeight; 165: } 166: 167: // タイプを返す 168: public int getType() { 169: return pieceType; 170: } 171: 172: public void onDraw(Canvas canvas) { …… (4) 173: super.onDraw(canvas); 174: Paint p = new Paint(); 175: p.setColor(Color.WHITE); 176: canvas.drawLine(0, 0, this.getWidth(), 0, p); 177: canvas.drawLine(0, 0, 0, this.getHeight(), p); 178: p.setColor(Color.BLACK); 179: canvas.drawLine(this.getWidth() - 1, 0, this.getWidth() - 1, this.getHeight(), p); 180: canvas.drawLine(0, this.getHeight() - 1, this.getWidth(), this.getHeight() - 1, p); 181: } 182: }
- TextViewをサブクラス化したPieceクラスを作る
- コンストラクター。127行目まで。タイプと位置を指定して、ピースを作る。タイプが0なら空き、1ならば父、2ならば母...というぐあい。コードは長いが、やっていることは単純。
- これ以降がPieceクラスで追加したメソッド。170行目まで。ピースの位置を取得したり、動かしたりするのに使う。やはり、コードは長いが、やっていることは単純。
- 描画のためのコード。onDrawメソッドをオーバーライド(描画が必要になったら自動的に呼び出される)。立体的に見せるために、左と上を白い線に、右と下を黒い線にしているだけ。
次にメインのコードです。Pieceクラスのオブジェクトを作成し、タッチされたらピースを動かします。OnClickListenerでは、オブジェクトのどの位置がクリックされたかが検出できないので、OnTouchListenerを使っています。たとえば、上と右に空きがあるときに、オブジェクトの上のほうをタッチすれば上に移動し、右のほうをタッチすれば右に移動できるようにするためです。
やはり、コードは長く、359行もあるので、1つ1つ解説することはできませんが、やっていることは単純です。タッチされた方向に空きピースがあれば、ピースを入れ替え、「娘」が出口にたどり着いたかを調べているだけです。15パズルのようなアプリケーションであれば、ピースのサイズはすべて同じですが、このアプリケーションではピースのサイズが異なるので、ちゃんと動かせるかどうかを調べるコードが少し複雑になっています。「……」の後の太字のキャプションをざっと眺めてポイントをつかんでください。
Java > java/com.example.sample.boxgirl/MainActivity.java
1: package com.example.sample.boxgirl; ; 16: import java.util.Vector; 17: 18: public class MainActivity extends ActionBarActivity implements View.OnTouchListener { …… (1) 19: 20: private Vector<Piece> p = new Vector<Piece>(); …… (2) 21: 22: @Override 23: protected void onCreate(Bundle savedInstanceState) { 24: super.onCreate(savedInstanceState); 25: RelativeLayout r = new RelativeLayout(this); 26: r.setGravity(Gravity.CENTER); 27: setContentView(r); 28: initialize(); …… (3) 29: for (int i = 0; i < 12; i++) { …… (4) 30: r.addView(p.get(i)); 31: p.get(i).setOnTouchListener(this); 32: } 33: TextView exitView = new TextView(this); // 出口(無理矢理設置) 34: r.addView(exitView); 35: 36: DisplayMetrics metrics = getResources().getDisplayMetrics(); 37: int gridSize = (int)(metrics.density * 64); // 1グリッドの幅と高さ 38: 39: RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) exitView.getLayoutParams(); 40: lp.leftMargin = 1 * gridSize; 41: lp.topMargin = 5 * gridSize; 42: lp.width = 2 * gridSize; 43: exitView.setLayoutParams(lp); 44: exitView.setText("出 口"); 45: exitView.setGravity(Gravity.CENTER_HORIZONTAL); 46: exitView.setBackgroundColor(Color.YELLOW); 47: exitView.setVisibility(View.VISIBLE); 48: 49: } 50: 51: private void initialize() { …… (5) 52: p.add(0, new Piece(this, 1, 0, 0)); // 父 53: p.add(1, new Piece(this, 2, 3, 0)); // 母 54: p.add(2, new Piece(this, 3, 0, 2)); // 祖父 55: p.add(3, new Piece(this, 3, 3, 2)); // 祖母 56: p.add(4, new Piece(this, 4, 0, 4)); // 弟1 57: p.add(5, new Piece(this, 4, 1, 3)); // 弟2 58: p.add(6, new Piece(this, 4, 2, 3)); // 弟3 59: p.add(7, new Piece(this, 4, 3, 4)); // 弟4 60: p.add(8, new Piece(this, 5, 1, 2)); // 執事 61: p.add(9, new Piece(this, 6, 1, 0)); // 娘 62: p.add(10, new Piece(this, 0, 1, 4)); // 空白1 63: p.add(11, new Piece(this, 0, 2, 4)); // 空白2 64: } 65: 66: public boolean onTouch(View v, MotionEvent event) { … (6) 67: Piece s = (Piece) v; 68: if (s.getText() == "") return false; // 空白なら何もしない 69: double delta = (double) s.getHeight() / s.getWidth(); // 傾き …… (7) 70: // クリックされた位置の近くを調べる 71: double y = event.getY(); …… (8) 72: double y1 = delta * event.getX(); …… (9) 73: double y2 = (-delta) * event.getX() + s.getHeight(); 74: if (y < y1 && y < y2) { // 上 …… (10) 75: if (checkMoveUp(s)) { 76: checkClear(); 77: return false; 78: } 79: } else if (y < y1 && y >= y2) { // 右 80: if (checkMoveRight(s)) { 81: checkClear(); 82: return false; 83: } 84: } else if (y >= y1 && y < y2) { // 左 85: if (checkMoveLeft(s)) { 86: checkClear(); 87: return false; 88: } 89: } else if (y >= y1 && y >= y2) { // 下 90: if (checkMoveDown(s)) { 91: checkClear(); 92: return false; 93: } 94: } 95: // 見つからなかったら順番に(汚いコードだが動くことは動く) 96: if (checkMoveUp(s)) { …… (11) 97: checkClear(); …… (12) 98: return false; // 上 99: } 100: if (checkMoveRight(s)) { 101: checkClear(); 102: return false; // 右 103: } 104: if (checkMoveLeft(s)) { 105: checkClear(); 106: return false; // 左 107: } 108: if (checkMoveDown(s)) { 109: checkClear(); 110: return false; // 下 111: } 112: return false; 113: } 114: 115: private boolean checkMoveUp(Piece curPiece) { // 上に動けるかどうかを調べる…… (13) 116: int sp1x = p.get(10).getXPos(); // 空白1のx位置 117: int sp1y = p.get(10).getYPos(); // 空白1のy位置 118: int sp2x = p.get(11).getXPos(); // 空白2のx位置 119: int sp2y = p.get(11).getYPos(); // 空白2のy位置 120: int curx = curPiece.getXPos(); // クリックしたピースのx位置 121: int cury = curPiece.getYPos(); // クリックしたピースのy位置 122: int curWidth = curPiece.getWidthByGrid(); // クリックしたピースの幅 123: int curHeight = curPiece.getHeightByGrid(); // クリックしたピースの高さ 124: 125: // 上に動けるか(幅1のスペースがあるか) 126: if (curWidth == 1) { // 幅が1なら 127: if (curx == sp1x && cury - 1 == sp1y) { //x位置が同じで1つ上 128: // 上に空白のピースがある。上と入れ替え 129: swapV(curPiece, p.get(10)); 130: return true; 131: } else if (curx == sp2x && cury - 1 == sp2y) { //x位置が同じで1つ上 132: // 上に空白のピースがある。上と入れ替え 133: swapV(curPiece, p.get(11)); 134: return true; 135: } 136: } 137: 138: if (curWidth == 2 && sp1x < sp2x) { // 幅が2で空白1が左、空白2が右 139: if (curx == sp1x && curx + 1 == sp2x && 140: cury - 1 == sp1y && cury - 1 == sp2y) { 141: // 上に隣り合って空白のピースがある。上と入れ替え 142: swapV2(curPiece, p.get(10), p.get(11)); 143: return true; 144: } 145: } 146: if (curWidth == 2 && sp1x > sp2x) { // 幅が2で空白1が右、空白2が左 147: if (curx == sp2x && curx + 1 == sp1x && 148: cury - 1 == sp1y && cury - 1 == sp2y) { 149: // 上に隣り合って空白のピースがある。上と入れ替え 150: swapV2(curPiece, p.get(10), p.get(11)); 151: return true; 152: } 153: } 154: return false; // 動かせなかった 155: } 156: 157: private boolean checkMoveDown(Piece curPiece) { …… (14) : (向きが異なるだけで、checkMoveUpメソッドと同様) : 196: } 197: 198: private boolean checkMoveLeft(Piece curPiece) { …… (15) : (向きが異なるだけで、checkMoveUpメソッドと同様) : 237: } 238: 239: private boolean checkMoveRight(Piece curPiece) { : (向きが異なるだけで、checkMoveUpメソッドと同様) : 278: } 279: 280: private void swapV(Piece a, Piece b) { …… (16) 281: int ay = a.getYPos(); 282: int by = b.getYPos(); 283: if (ay < by) { // aが上 284: b.setYPos(ay); // 下にあるものを上に 285: a.setYPos(ay + b.getHeightByGrid()); // 上+ピースの高さ 286: } else { // aが下 287: a.setYPos(by); // 下にあるものを上に 288: b.setYPos(by + a.getHeightByGrid()); // 上+ピースの高さ 289: } 290: } 291: 292: private void swapV2(Piece a, Piece sp1, Piece sp2) { …… (17) 293: int ay = a.getYPos(); 294: int sp1y = sp1.getYPos(); // sp1とsp2の垂直位置は同じ 295: if (ay < sp1y) { // aが上 296: sp1.setYPos(ay); 297: sp2.setYPos(ay); 298: a.setYPos(ay + sp1.getHeightByGrid()); 299: } else { // aが下 300: a.setYPos(sp1y); 301: sp1.setYPos(sp1y + a.getHeightByGrid()); 302: sp2.setYPos(sp1y + a.getHeightByGrid()); 303: } 304: } 305: 306: private void swapH(Piece a, Piece b) {…… (18) : (向きが異なるだけで、swapVメソッドと同様) : 316: } 317: 318: private void swapH2(Piece a, Piece sp1, Piece sp2) { …… (19) : (向きが異なるだけで、swapV2メソッドと同様) : 330: } 331: 332: private void checkClear() { 333: if (p.get(9).getXPos() == 1 && p.get(9).getYPos() == 3) { 334: Toast.makeText(this, "おめでとう!", Toast.LENGTH_SHORT).show(); 335: } 336: } : 359: }
- アクティビティでOnTouchListenerをインプリメントする。OnClickListenerではクリックされたことは検出できるが、どの位置がクリックされたかは検出しないので、タッチされた位置を知るためにこちらを使う
- ピースはVectorオブジェクトとする。配列と似ているが、addメソッドなどが使えるので、個々の要素を扱いやすい。
- ピースを作る。51行目以降のコードが実行される
- すべてのピースをビューに追加しOnTouchListenerをセット
- ピースを作って、Vectorオブジェクトに追加
- タッチされたら自動的に実行されるonTouchメソッドをオーバーライド
- オブジェクトの対角線の傾き(deltaが左下から右上、-deltaが左上から右下の対角線の傾きになる)
- オブジェクトのどの位置がクリックされたか(Y位置)
- オブジェクトのどの位置がクリックされたか(X位置)、それに傾きを掛けて、その場合のY位置を求める)
- 対角線より上をクリックしたか調べる(以下同様)
- 動きたい方向が空いているかどうかを調べる。113行目以降のコードが実行される
- 「娘」を外に出せたか調べる。340行目以降のコードが実行される。以下同様。
- 上が空白のピースであれば、ピースを動かす。
- 下が空白のピースであれば、ピースを動かす。
- 左が空白のピースであれば、ピースを動かす。
- 実際にピースを動かすためのコード(幅1のピースを上下で交換)
- 実際にピースを動かすためのコード(幅2のピースを上下で交換)
- 実際にピースを動かすためのコード(高さ1のピースを左右で交換)
- 実際にピースを動かすためのコード(高さ2のピースを左右で交換)
オブジェクト上のどの位置をタッチしたかを知る
66行目~94行目が1つの重要なポイントです。オブジェクトにタッチされたときに呼び出される(6)のonTouchメソッドの引数eventにはgetXメソッドやgetYメソッドがあり、タッチされた位置を知ることができます。オブジェクトの上下左右のどの位置をタッチしたかは、以下のように対角線を描けば、その上か下かで調べることができます。

この例であれば、小さい●の位置(タッチされた位置)は赤い対角線(左上から右下)よりも上で、青い対角線(左下から右上)よりも下です。ということは、オブジェクトの右がクリックされたことがわかります。
まず、●のx位置から、それに対する対角線のY位置(y1の値)を求め、それと、●のy位置とを比較すれば上か下かの判定ができますね。ただし、位置はオブジェクトの左上を(0,0)としているので、値の大きい方が下になることに注意が必要です。
タッチした方向に空きがあるかを調べる
もう1つのポイントは115行目の(13)以降です。上に空白のピースがあって入れ替えができるかどうかを調べるコードです。タッチしたオブジェクトが「父」や「弟」のように、幅1のものであれば、真上だけを調べれば入れ替えができるかどうかが分かります。しかし、「娘」や「執事」のように、幅が2ある場合は、上の2つのピースが両方空きであるかを調べる必要があります。if文による判定が複雑になっていますが、やっているのはそれだけです。
それ以降のコードも、下に動かせるか、左に動かせるか、右に動かせるかを順に調べているだけなので、よく似たコードになっています。
なお、実際にピースの入れ替えをするのはswapV(幅1のピースを上下入れ替え)、swapV2(幅2のピースを上下入れ替え)、swapH(高さ1のピースを左右入れ替え)、swapH2(高さ2のピースを左右入れ替え)という各メソッドです。
今回解説したのは書籍のサポートページにある「さらに高度なアプリ」の中の「BoxGirl」というサンプルです。本書で扱っている他のアプリもぜひ遊んでみてください。
http://book.impress.co.jp/books/1114101120_4
このプログラムは、デスクトップアプリケーションとしてSwingを使って作ったものを、Androidアプリケーションに改造したものです。プログラミングの授業で「このへんまでならがんばればできるよ」というサンプルとして示したものなので(解説のために作ったコードではないので)、あまり整ったコードにはなっていません。こころざしのある方はぜひ、簡潔なコードで同じことができるように改造してみてください。
※サンプルプログラムの絵は、本書に出てくるものも含め、筆者の手によるものです。イラストレーターさんの絵と比べるとクオリティが落ちますが、そのあたりはご容赦のほど。なお、自分で絵を描いてリソースを変更すると、自分だけの「箱入り娘」が作れますよ。
この記事のもとになった書籍はこちら! | |
---|---|
![]() 羽山 博/めじろまち 著 |
イラストでよくわかるAndroidアプリのつくり方—Android Studio対応版プログラミング未経験でも大丈夫! Android Studio対応のAndroidアプリ開発入門、決定版。好評だった前作『イラストでよくわかるAndroidアプリのつくり方』に改訂版が登場。親しみやすいイラストやステップバイステップでの丁寧な解説といった基本コンセプトを踏襲しつつ、最新版のSDKや、Androidの新しい開発環境である「Android Studio」に対応させました! Androidのプログラムを作りながら、自然にJavaというプログラム言語の知識が身につくようになっています。 |