2012年2月26日日曜日

リソースの描画

サンプルの「Snake」を題材として、アプリ作成の基本をまとめるシリーズです。

第五回目は「リソースの描画」です。


まずは、リソースをCanvasに描画する手順についてです。

リソースをBitmapオブジェクトとして取得する場合と、Drawableオブジェクトとして取得する場合とに分けることができます。

◆リソースをBitmapオブジェクトとして取得して描画する場合
①リソースオブジェクトの取得
Resource res = Context.getResource()

②Bitmapオブジェクトの取得
Bitmap image = BitmapFactory.decodeResource(res, R.drawable.画像ファイル名)

③CanvasクラスのメソッドでCanvasに描画
Canvas.drawBitmap(image, int x, int y, Paint p)


◆リソースをDrawableオブジェクトとして取得して描画する場合
①リソースオブジェクトの取得
Resource res = Context.getResource()

②Drawableオブジェクトの取得
Drawable image = res.getDrawable(R.drawable.画像ファイル名)

③DrawableオブジェクトのメソッドでCanvasに描画
image.setBounds(int left, int top, int right, int bottom)
image.draw(Canvas canvas)



次に、「Snake」に於けるリソースの描画です。

「Snake」では、リソースをそのままCanvasに描画してはいません。
まずは、リソースからDrawableオブジェクトとして取得したimageを、CanvasではなくBitmapに、画像データとして描画しています。

【TileView.java】
public void loadTile(int key, Drawable tile) {
Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize, Bitmap.Config.ARGB_8888);//...①
Canvas canvas = new Canvas(bitmap);//...②
tile.setBounds(0, 0, mTileSize, mTileSize);
tile.draw(canvas);//...③
mTileArray[key] = bitmap;
}

①createBitmap()メソッドでBitmapオブジェクト作成
引数のBitmap.Config.ARGB_8888は、Bitmapにフルカラーの色情報を持たせるための設定です。

②Bitmapオブジェクトを引数にして、Canvasオブジェクト作成

③Drawableオブジェクトの描画
引数のcanvasは、Bitmapオブジェクトを引数にして作成したCanvasオブジェクトのため、DrawableオブジェクトはBitmapに描画されます。


そして、このBitmapをCanvasに対して描画しています。

【TileView.java】
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int x = 0; x < mXTileCount; x += 1) {
for (int y = 0; y < mYTileCount; y += 1) {
if (mTileGrid[x][y] > 0) {
canvas.drawBitmap(mTileArray[mTileGrid[x][y]],
mXOffset + x * mTileSize,
mYOffset + y * mTileSize,
mPaint);
}
}
}
}

2012年2月24日金曜日

キーイベント

サンプルの「Snake」を題材として、アプリ作成の基本をまとめるシリーズです。

第四回目は「キーイベント」です。

「Snake」では、ゲーム操作を矢印キーで行うことになっています。

キーが押されると、それを知らせるイベントが発生します。
まずは、そのイベントを受け取れるようにする必要があります。

Viewがキーのイベントを受け取れるようにするには、以下のメソッドを呼び出して、キー操作やトラックボールでフォーカスできる許可を与える必要があります。

setFocusable(true)


「Snake」では、以下のように設定しています。

【SnakeView.java】
private void initSnakeView() {
setFocusable(true);
---------
}

※尚、画面タッチによるフォーカスを許可するメソッドは、setFocusableInTouchMode(true)です。


これで、Viewでイベントを受け取るための準備ができました。
では、次にイベントを受け取ってからの処理をみてみましょう。

キーが押さて、イベントが発生するとonKeyDownメソッドが呼び出されます。

boolean onKeyDown(int keyCode, KeyEvent event)


受け取る引数の内、keyCodeは操作されたキーを識別する整数値です。
keyCodeの値によって条件分岐すれば、操作されたキー毎に異なる処理をさせることができます。

keyCodeは、KeyEventクラスの中で定数として定義されています。
以下は、定数の一部で、「Snake」で利用されています。

KEYCODE_DPAD_UP 上向き矢印キー
KEYCODE_DPAD_DOWN 下向き矢印キー
KEYCODE_DPAD_LEFT 左向き矢印キー
KEYCODE_DPAD_RIGHT 右向き矢印キー


以下は、「Snake」でのonKeyDownメソッドです。
キーを識別して、ヘビの進む方向を保持しているmNextDirectionメンバ変数を更新しているのがわかります。

【SnakeView.java】
public boolean onKeyDown(int keyCode, KeyEvent msg) {
-------------------------------

if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
if (mDirection != EAST) {
mNextDirection = WEST;
}
return (true);
}

if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if (mDirection != WEST) {
mNextDirection = EAST;
}
return (true);
}

return super.onKeyDown(keyCode, msg);
}

onKeyDownの戻り値はtrueを返していますが、イベントを処理した場合はtrue、処理しなかった場合はfalseを返すように定義します。

2012年2月17日金曜日

カスタムView

サンプルの「Snake」を題材として、アプリ作成の基本をまとめるシリーズです。

第三回目は「カスタムView」です。

カスタムViewとは、TextViewやButtonなどの標準のViewではなく、android.view.Viewを拡張して独自に作成したViewのことです。
「Snake」では、TileViewというカスタムViewを作成しています。

・Viewの属性
例えば、標準のTextViewには、textColorやtextSizeなどの属性が用意されています。
カスタムViewに於いても独自の属性を用意することができます。

属性は、res/values/attrs.xmlで作成します。
「Snake」では、以下ように定義しています。

【attrs.xml】
< ?xml version="1.0" encoding="utf-8"?>
< resources>
< declare-styleable name="TileView">
< attr name="tileSize" format="integer" />
< /declare-styleable>
< /resources>

「declare-styleable name」にはカスタムViewのクラス名を、「attr name」には作成する属性名を、「format」には属性のフォーマット名を設定します。

※属性のフォーマット:integer,float,boolean,string,color,dimension

・レイアウト
「Snake」では、カスタムViewをXMLファイルで、以下のようにレイアウトを定義しています。
レイアウトのSnakeViewは、作成したTitleViewを、更に拡張して定義したカスタムViewです。
つまり、カスタムViewを拡張したカスタムViewとなっています。

【snake_layout.xml】
< FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:snake="http://schemas.android.com/apk/res/com.example.android.snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

< com.example.android.snake.SnakeView
android:id="@+id/snake"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
snake:tileSize="12"
/>

< /FrameLayout>

作成した属性に対して、tileSize="12"を設定しています。


例えば、下図は属性値をtileSize="24"に設定した場合です。


・コンストラクタ
カスタムViewをXMLファイルでレイアウトするときのコンストラクタの引数に注意してください。
Context contextとAttributeSet attrsが必要になります。

「Snake」では、以下のように定義しています。

【SnakeView.java】
public SnakeView(Context context, AttributeSet attrs) {
super(context, attrs);
initSnakeView();
}


super(context, attrs)で
親クラスのTileViewへ引き渡しています。

【TileView.java】
public TileView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView);
mTileSize = a.getInt(R.styleable.TileView_tileSize, 12);
a.recycle();
}

※R.styleable.TileView -->R.styleable.[declare-styleable name]
※R.styleable.TileView_tileSize -->R.styleable.[declare-styleable name]_[attr name]

obtainStyledAttributes()メソッドによって、TypedArrayインスタンスを作成します。
getInt()メソッドで属性値を取得しています。


属性フォーマットの違いによって、以下のメゾッドなどを利用します。

getInt(int index, int defValue)
getFloat(int index, float defValue)
getBoolean(int index, boolean defValue)
getString(int index)
getColor(int index, int defValue)
getDimensionPixelOffset(int index, int defValue)

2012年2月15日水曜日

定期処理ハンドラ

サンプルの「Snake」を題材として、アプリ作成の基本をまとめるシリーズです。

第二回目は「定期処理ハンドラ」です。

定期処理ハンドラとは、Handlerクラスを利用して、一定の間隔で処理を行わせる仕組みのことです。

例えば、場面場面でキャラクターの出現数の違いによって、処理の重さが異なる場合があります。
処理の重さの違いでキャラクターの動きにムラは出したくありません。
また、端末の処理能力の違いでキャラクターの動く速度が変わってしまうことも避けたいです。

その解決方法の一つとして、定期処理ハンドラの仕組みがあります。
「Snake」では、以下のように定期処理ハンドラを使用しています。

【SnakeView.java】

class RefreshHandler extends Handler {

@Override
public void handleMessage(Message msg) {
SnakeView.this.update();
SnakeView.this.invalidate();
}

public void sleep(long delayMillis) {
this.removeMessages(0);
sendMessageDelayed(obtainMessage(0), delayMillis);
}
};


public void update() {
if (mMode == RUNNING) {
long now = System.currentTimeMillis();

if (now - mLastMove > mMoveDelay) {
clearTiles();
updateWalls();
updateSnake();
updateApples();
mLastMove = now;
}
mRedrawHandler.sleep(mMoveDelay);
}
}


sendMessageDelayedメソッドによって、指定時間(delayMillis)後にMessageオブジェクトを送信しています。
Messageオブジェクトは、handleMessageオブジェクトで受信されます。
handleMessageオブジェクトから呼び出されるupdateメソッドでsendMessageDelayedメソッドを呼び出しています。
これを繰り返すことで、指定時間(delayMillis)の間隔で定期的な処理を実現しています。

この定期処理の中で、アップデートやViewの再描画を実施しています。
SnakeView.this.update();
SnakeView.this.invalidate();

前回のアップデートからの経過時間が指定時間(mMoveDelay)より大きい場合のみ、アップデートを実施すれば更新間隔を一定に保つことができます。

sendMessageDelayedメソッドによって送信するMessageオブジェクトは、以下のpublicなフィールドを持つMessageクラスのオブジェクトです。

public int what;
public int arg1;
public int arg2;
public Object obj;

フィールドには、sendMessageDelayedメソッドによって引き渡したい値を代入します。
定期処理を実現するだけならば、引き渡す値は何でもかまいません。
「Snake」では、what=0の値を送信しています。


因みに、指定時間(mMoveDelay)は、以下のように初期設定してあります。
mMoveDelay = 600;

アップデート処理の中で、ヘビがリンゴを食べる度に指定時間(mMoveDelay)を減少させています。
mMoveDelay *= 0.9;

次第に定期処理の間隔を短くして、ヘビの動きを速くすることで、ゲームの面白さを実現しています。


最後に、Messageオブジェクトの詳細です。

Messageオブジェクトの作成とフィールド値の設定は以下のようにします。

Message msg = new Message();
msg.what=123;
msg.arg1=1;
msg.arg1=2;
msg.obj="object";

obtainMessageメソッドを利用すれば、以下のように簡単に出来ます。

Message msg = obtainMessage(123,1,2,"object");


obtainMessageメソッドは以下のようにオーバーロードされています。

obtainMessage(int what)
obtainMessage(int what, Object obj)
obtainMessage(int what, int arg1, int arg2)
obtainMessage(int what, int arg1, int arg2, Object obj)


「Snake」では、obtainMessage(int what)でオブジェクトの作成とフィールド値の設定を行っています。

2012年2月12日日曜日

インスタンスの保存

androidのSDKには多くのサンプルが用意されています。
これはとても良い教材であり、アプリを作成する際のヒントとして、とても重宝しています。

今回から数回にわたり、サンプルの「Snake」を題材として、アプリ作成の基本をまとめてみます。


第一回目は「インスタンスの保存」です。

インスタンスの保存は何故必要なのでしょうか?

端末の向きが変わって画面が回転した場合は、いったんActivityは破棄されて、onCreate()メソッドで再作成されます。
また、長い時間背面に移動していたActivityが前面に戻る様な時も、いったんActivityは破棄されて、onCreate()メソッドで再作成されます。
Activityが破棄されるということは、インスタンスが保持していたデータも消失してしまうことを意味します。

そこでインスタンスの保存が必要になるのです。


「Snake」の例でいうと、背面に移動していたActivityが前面に戻ってきたとき、ゲームの続きからプレイできるように、ヘビとリンゴの位置情報やスコアなどを保存しておく必要があります。

以下のように保存しています。

【Snake.java】

@Override
public void onSaveInstanceState(Bundle outState) {
//Store the game state
outState.putBundle(ICICLE_KEY, mSnakeView.saveState());
}

【SnakeView.java】

public Bundle saveState() {
Bundle map = new Bundle();
map.putIntArray("mAppleList", coordArrayListToArray(mAppleList));
map.putInt("mDirection", Integer.valueOf(mDirection));
map.putInt("mNextDirection", Integer.valueOf(mNextDirection));
map.putLong("mMoveDelay", Long.valueOf(mMoveDelay));
map.putLong("mScore", Long.valueOf(mScore));
map.putIntArray("mSnakeTrail", coordArrayListToArray(mSnakeTrail));
return map;
}

Activityが背面に移動する際に呼び出されるonSaveInstanceState()メソッドをオーバーライドして、その中で必要なデータを保存しています。
引数のBundleオブジェクトにヘビやリンゴの状態、スコアなどを保存しているのがわかります。


保存したデータは、以下のように読み出しています。

【Snake.java】

@Override
public void onCreate(Bundle savedInstanceState) {

--------------

// We are being restored
Bundle map = savedInstanceState.getBundle(ICICLE_KEY);
if (map != null) {
mSnakeView.restoreState(map);
}

--------------

}


【SnakeView.java】

public void restoreState(Bundle icicle) {
setMode(PAUSE);
mAppleList = coordArrayToArrayList(icicle.getIntArray("mAppleList"));
mDirection = icicle.getInt("mDirection");
mNextDirection = icicle.getInt("mNextDirection");
mMoveDelay = icicle.getLong("mMoveDelay");
mScore = icicle.getLong("mScore");
mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail"));
}


onCreate()メソッドの引数であるBundleオブジェクトから、ヘビやリンゴの状態、スコアなどを読み出しているのがわかります。