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

2015年7月8日(水)
羽山 博(はやま ひろし)
イラストでよくわかるAndroidアプリのつくり方—Android Studio対応版
Amazon詳細ページへ
この記事では、書籍『イラストでよくわかるAndroidアプリのつくり方—Android Studio対応版』の解説内容をもとに、本書で説明しきれなかったサンプルアプリを解説しています。

「箱入り娘」は古くからあるパズルで、大きさの違ういくつかのピースを動かし、「娘」と書かれたピースを外に出すゲームです。とりあえず、遊んでみてください。「執事」や「父」「母」などが邪魔をして、なかなか娘を外に出すことができません(必勝法はあるようですが)。

BoxGirlの実行例

グレーの部分が空きです。空きに接しているピースをクリックすると、ピースが空きの場所に動きます。上と右など2方向に空きがある場合は、ピースの上のほうをクリックすると上に移動し、右のほうをクリックすると右に移動します。

アプリは以下のような設定で作成してあります。表にない項目については、設定を変更していません。

項目名設定する内容
Application NameBoxGirl
Company Domainsample.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: }
  1. TextViewをサブクラス化したPieceクラスを作る
  2. コンストラクター。127行目まで。タイプと位置を指定して、ピースを作る。タイプが0なら空き、1ならば父、2ならば母...というぐあい。コードは長いが、やっていることは単純。
  3. これ以降がPieceクラスで追加したメソッド。170行目まで。ピースの位置を取得したり、動かしたりするのに使う。やはり、コードは長いが、やっていることは単純。
  4. 描画のためのコード。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: }
  1. アクティビティでOnTouchListenerをインプリメントする。OnClickListenerではクリックされたことは検出できるが、どの位置がクリックされたかは検出しないので、タッチされた位置を知るためにこちらを使う
  2. ピースはVectorオブジェクトとする。配列と似ているが、addメソッドなどが使えるので、個々の要素を扱いやすい。
  3. ピースを作る。51行目以降のコードが実行される
  4. すべてのピースをビューに追加しOnTouchListenerをセット
  5. ピースを作って、Vectorオブジェクトに追加
  6. タッチされたら自動的に実行されるonTouchメソッドをオーバーライド
  7. オブジェクトの対角線の傾き(deltaが左下から右上、-deltaが左上から右下の対角線の傾きになる)
  8. オブジェクトのどの位置がクリックされたか(Y位置)
  9. オブジェクトのどの位置がクリックされたか(X位置)、それに傾きを掛けて、その場合のY位置を求める)
  10. 対角線より上をクリックしたか調べる(以下同様)
  11. 動きたい方向が空いているかどうかを調べる。113行目以降のコードが実行される
  12. 「娘」を外に出せたか調べる。340行目以降のコードが実行される。以下同様。
  13. 上が空白のピースであれば、ピースを動かす。
  14. 下が空白のピースであれば、ピースを動かす。
  15. 左が空白のピースであれば、ピースを動かす。
  16. 実際にピースを動かすためのコード(幅1のピースを上下で交換)
  17. 実際にピースを動かすためのコード(幅2のピースを上下で交換)
  18. 実際にピースを動かすためのコード(高さ1のピースを左右で交換)
  19. 実際にピースを動かすためのコード(高さ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対応版

羽山 博/めじろまち 著
価格:2,200円+税
発売日:2015年6月5日発売
ISBN:978-4-8443-3813-0
発行:インプレス

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

プログラミング未経験でも大丈夫! Android Studio対応のAndroidアプリ開発入門、決定版。好評だった前作『イラストでよくわかるAndroidアプリのつくり方』に改訂版が登場。親しみやすいイラストやステップバイステップでの丁寧な解説といった基本コンセプトを踏襲しつつ、最新版のSDKや、Androidの新しい開発環境である「Android Studio」に対応させました! Androidのプログラムを作りながら、自然にJavaというプログラム言語の知識が身につくようになっています。

Amazon詳細ページへImpress詳細ページへ

著者
羽山 博(はやま ひろし)

京都大学文学部哲学科(心理学専攻)卒業後、NECでユーザー教育や社内SE教育を担当したのち、ライターとして独立。ソフトウェアの基本からプログラミング、認知科学、統計学まで幅広く執筆。読者の側に立った分かりやすい表現を心がけている。2006年に東京大学大学院学際情報学府博士課程を単位取得後退学。現在、有限会社ローグ・インターナショナル代表取締役、日本大学、青山学院大学、お茶の水女子大学講師。 最近の趣味は書道、絵画、ウクレレ、ジャグリング、献血。

連載バックナンバー

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています