「箱入り娘」は古くからあるパズルで、大きさの違ういくつかのピースを動かし、「娘」と書かれたピースを外に出すゲームです。とりあえず、遊んでみてください。「執事」や「父」「母」などが邪魔をして、なかなか娘を外に出すことができません(必勝法はあるようですが)。
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; |
18 : public class Piece extends TextView { …… ( 1 ) |
19 : private String pieceName; |
20 : private int pieceType; |
21 : private int pieceWidth; |
22 : private int pieceHeight; |
27 : DisplayMetrics metrics = getResources().getDisplayMetrics(); … 画面の解像度の比率(dp/pixel)を求める |
28 : private int gridSize = ( int ) (metrics.density * 64 ); |
30 : public Piece(Context c, int type, int x, int y) { …… ( 2 ) |
42 : pieceName = "父" ; ……「父」は幅が 1 、高さが 2 のピース。以下同様。 |
45 : imgId = R.drawable.father; |
51 : imgId = R.drawable.mother; |
(同様にして、タイプをもとに各ピースの設定を変える) |
101 : imgId = R.drawable.daughter; |
108 : this .setLayoutParams( new LayoutParams(pieceWidth * gridSize, pieceHeight * gridSize)); |
110 : lp = (LayoutParams) this .getLayoutParams(); |
111 : lp.leftMargin = x * gridSize; |
112 : lp.topMargin = y * gridSize; |
113 : this .setLayoutParams(lp); |
115 : this .setText(pieceName); |
116 : this .setBackgroundResource(imgId); |
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); |
126 : this .setVisibility(View.VISIBLE); |
131 : public int getXPos() { |
135 : public int getYPos() { |
140 : public void setXPos( int x) { |
143 : lp.leftMargin = x * gridSize; |
144 : lp.topMargin = y * gridSize; |
145 : this .setLayoutParams(lp); |
149 : public void setYPos( int y) { |
152 : lp.leftMargin = x * gridSize; |
153 : lp.topMargin = y * gridSize; |
154 : this .setLayoutParams(lp); |
159 : public int getWidthByGrid() { |
163 : public int getHeightByGrid() { |
168 : public int getType() { |
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); |
- 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; |
18 : public class MainActivity extends ActionBarActivity implements View.OnTouchListener { …… ( 1 ) |
20 : private Vector<Piece> p = new Vector<Piece>(); …… ( 2 ) |
23 : protected void onCreate(Bundle savedInstanceState) { |
24 : super .onCreate(savedInstanceState); |
25 : RelativeLayout r = new RelativeLayout( this ); |
26 : r.setGravity(Gravity.CENTER); |
29 : for ( int i = 0 ; i < 12 ; i++) { …… ( 4 ) |
31 : p.get(i).setOnTouchListener( this ); |
33 : TextView exitView = new TextView( this ); |
36 : DisplayMetrics metrics = getResources().getDisplayMetrics(); |
37 : int gridSize = ( int )(metrics.density * 64 ); |
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); |
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 )); |
57 : p.add( 5 , new Piece( this , 4 , 1 , 3 )); |
58 : p.add( 6 , new Piece( this , 4 , 2 , 3 )); |
59 : p.add( 7 , new Piece( this , 4 , 3 , 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 )); |
63 : p.add( 11 , new Piece( this , 0 , 2 , 4 )); |
66 : public boolean onTouch(View v, MotionEvent event) { … ( 6 ) |
68 : if (s.getText() == "" ) return false ; |
69 : double delta = ( double ) s.getHeight() / s.getWidth(); |
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) { |
75 : if (checkMoveUp(s)) { |
79 : } else if (y < y1 && y >= y2) { |
80 : if (checkMoveRight(s)) { |
84 : } else if (y >= y1 && y < y2) { |
85 : if (checkMoveLeft(s)) { |
89 : } else if (y >= y1 && y >= y2) { |
90 : if (checkMoveDown(s)) { |
96 : if (checkMoveUp(s)) { …… ( 11 ) |
97 : checkClear(); …… ( 12 ) |
100 : if (checkMoveRight(s)) { |
104 : if (checkMoveLeft(s)) { |
108 : if (checkMoveDown(s)) { |
115 : private boolean checkMoveUp(Piece curPiece) { |
116 : int sp1x = p.get( 10 ).getXPos(); |
117 : int sp1y = p.get( 10 ).getYPos(); |
118 : int sp2x = p.get( 11 ).getXPos(); |
119 : int sp2y = p.get( 11 ).getYPos(); |
120 : int curx = curPiece.getXPos(); |
121 : int cury = curPiece.getYPos(); |
122 : int curWidth = curPiece.getWidthByGrid(); |
123 : int curHeight = curPiece.getHeightByGrid(); |
126 : if (curWidth == 1 ) { |
127 : if (curx == sp1x && cury - 1 == sp1y) { |
129 : swapV(curPiece, p.get( 10 )); |
131 : } else if (curx == sp2x && cury - 1 == sp2y) { |
133 : swapV(curPiece, p.get( 11 )); |
138 : if (curWidth == 2 && sp1x < sp2x) { |
139 : if (curx == sp1x && curx + 1 == sp2x && |
140 : cury - 1 == sp1y && cury - 1 == sp2y) { |
142 : swapV2(curPiece, p.get( 10 ), p.get( 11 )); |
146 : if (curWidth == 2 && sp1x > sp2x) { |
147 : if (curx == sp2x && curx + 1 == sp1x && |
148 : cury - 1 == sp1y && cury - 1 == sp2y) { |
150 : swapV2(curPiece, p.get( 10 ), p.get( 11 )); |
157 : private boolean checkMoveDown(Piece curPiece) { …… ( 14 ) |
(向きが異なるだけで、checkMoveUpメソッドと同様) |
198 : private boolean checkMoveLeft(Piece curPiece) { …… ( 15 ) |
(向きが異なるだけで、checkMoveUpメソッドと同様) |
239 : private boolean checkMoveRight(Piece curPiece) { |
(向きが異なるだけで、checkMoveUpメソッドと同様) |
280 : private void swapV(Piece a, Piece b) { …… ( 16 ) |
281 : int ay = a.getYPos(); |
282 : int by = b.getYPos(); |
285 : a.setYPos(ay + b.getHeightByGrid()); |
288 : b.setYPos(by + a.getHeightByGrid()); |
292 : private void swapV2(Piece a, Piece sp1, Piece sp2) { …… ( 17 ) |
293 : int ay = a.getYPos(); |
294 : int sp1y = sp1.getYPos(); |
298 : a.setYPos(ay + sp1.getHeightByGrid()); |
301 : sp1.setYPos(sp1y + a.getHeightByGrid()); |
302 : sp2.setYPos(sp1y + a.getHeightByGrid()); |
306 : private void swapH(Piece a, Piece b) {…… ( 18 ) |
318 : private void swapH2(Piece a, Piece sp1, Piece sp2) { …… ( 19 ) |
(向きが異なるだけで、swapV2メソッドと同様) |
332 : private void checkClear() { |
333 : if (p.get( 9 ).getXPos() == 1 && p.get( 9 ).getYPos() == 3 ) { |
334 : Toast.makeText( this , "おめでとう!" , Toast.LENGTH_SHORT).show(); |
- アクティビティで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アプリケーションに改造したものです。プログラミングの授業で「このへんまでならがんばればできるよ」というサンプルとして示したものなので(解説のために作ったコードではないので)、あまり整ったコードにはなっていません。こころざしのある方はぜひ、簡潔なコードで同じことができるように改造してみてください。
※サンプルプログラムの絵は、本書に出てくるものも含め、筆者の手によるものです。イラストレーターさんの絵と比べるとクオリティが落ちますが、そのあたりはご容赦のほど。なお、自分で絵を描いてリソースを変更すると、自分だけの「箱入り娘」が作れますよ。
この記事のもとになった書籍はこちら! |
羽山 博/めじろまち 著
価格:2,200円+税
発売日:2015年6月5日発売
ISBN:978-4-8443-3813-0
発行:インプレス
|
プログラミング未経験でも大丈夫! Android Studio対応のAndroidアプリ開発入門、決定版。好評だった前作『イラストでよくわかるAndroidアプリのつくり方』に改訂版が登場。親しみやすいイラストやステップバイステップでの丁寧な解説といった基本コンセプトを踏襲しつつ、最新版のSDKや、Androidの新しい開発環境である「Android Studio」に対応させました! Androidのプログラムを作りながら、自然にJavaというプログラム言語の知識が身につくようになっています。
 
|