んでもって、PreferenceActivity

今回はSharedPreferencesの回で作成したcodeを利用して、"Be Bastard"(SurfaceView, Handlerの回で拡張してきたapp)の設定画面を実装して行きたいと思います。ここではIconが動く早さを設定したり、Gameクリアまでに何回、Iconにtouchしなくてはならないかの設定をUserが設定し保存出来るようにするのが目的です。前回のSharedPreferencesの回では自前でListViewを利用した設定画面を作ろうかと思っていましたが、調べてみたところPrefereceActivityという便利なclassが存在していたので、前回作成したcodeを少し修正しています。

前置きが長くなりましたが

PreferenceActivityとは

設定画面の一覧を表示することに特化したActivityです。詳しくはここを参照してください。
注:AndroidにFragmentsという概念が加わりHoneyCombからはPreferenceActivityのAPIが変わっています。ここで利用しているのはHoneyComb前のAPIです。Fragmentsに関してはここ

実装の要点をまとめます

  1. PreferenceActivityを拡張したClassを作成する
  2. res/xml/配下に設定画面の項目を定義したxmlを作成。その種類には主に下記のようなものがある。詳しくはここを参照
  3. 作成したxmlをPreferenceActivity#addPreferencesFromResource(int) methodの引数にxmlのIDを指定し設定をする(注:このmethodもHoneyComb以降ではdeprecateしてます)

気づいた点をまとめます

  • xml内の各Preferenceの"android:key"属性にSharedPreferencesで使用されるkeyの値を指定する(SharedPreferences#get~(key, value)のkey)
  • ListPreferenceの"android:entries"および"android:entryValues"にはString Arrayを指定する
  • PreferenceActivityから保存されるSharedPreferencesはdefaultのSharedPreferencesなのでSharePreferecesの値を読みに行く際にはその点、注意

実装です

PreferenceActivityを拡張したclass

package com.android.practice;

import android.os.Bundle;
import android.preference.PreferenceActivity;

//PreferenceActivityを拡張したClassを作成する
public class MyPreferenceActivity extends PreferenceActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
                //作成したxmlをPreferenceActivity#addPreferencesFromResource(int) methodの引数にxmlのIDを指定し設定をする
		addPreferencesFromResource(R.xml.pref);
	}
	
}

res/xml配下のxml

<!-- res/xml/配下に設定画面の項目を定義したxmlを作成 -->
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
  xmlns:android="http://schemas.android.com/apk/res/android">
  <PreferenceCategory
    android:title="@string/settings_title">
    <!-- xml内の各Preferenceの"android:key"属性にSharedPreferencesで使用されるkeyの値を指定する -->
    <!-- ListPreferenceの"android:entries"および"android:entryValues"にはString Arrayを指定する -->
    <ListPreference android:key="Speed"
      android:title="@string/settings_speed_level_menu_title"
      android:dialogTitle="@string/settings_speed_level_menu_title"
      android:entries="@array/settings_list_entries"
      android:entryValues="@array/settings_list_entry_values"> 
    </ListPreference>
    <!-- xml内の各Preferenceの"android:key"属性にSharedPreferencesで使用されるkeyの値を指定する -->
    <!-- ListPreferenceの"android:entries"および"android:entryValues"にはString Arrayを指定する -->
    <ListPreference android:key="Clear_hit_counts"
      android:title="@string/settings_hit_count_to_clear_title"
      android:dialogTitle="@string/settings_hit_count_to_clear_title"
      android:entries="@array/settings_list_entries"
      android:entryValues="@array/settings_list_entry_values"> 
    </ListPreference>
  </PreferenceCategory>
</PreferenceScreen>

array.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <!-- ListPreferenceの"android:entries"および"android:entryValues"にはString Arrayを指定する -->
  <string-array name="settings_list_entries">
    <item>easy</item>
    <item>normal</item>
    <item>hard</item>
  </string-array>
  <!-- ListPreferenceの"android:entries"および"android:entryValues"にはString Arrayを指定する -->
  <string-array name="settings_list_entry_values">
    <item>easy</item>
    <item>normal</item>
    <item>hard</item>
  </string-array>
</resources>

SharedPreferencesを読むclass(SharedPreferencesの回に作成ものを修正)

package com.android.practice;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

public class GameSettings {
	
	private static final String TAG = "GameSettings";
	
	public static final String CLEAR_HIT_COUNT_LEVEL = "Clear_hit_counts";
	public static final int CLEAR_HIT_COUNT_LEVEL_INDEX = 0;
	public static final String CLEAR_HIT_COUNT_LEVEL_EASY = "easy";
	public static final String CLEAR_HIT_COUNT_LEVEL_NORMAL = "normal";
	public static final String CLEAR_HIT_COUNT_LEVEL_HARD = "hard";
	
	public static final String SPEED_LEVEL = "Speed";
	public static final int SPEED_LEVEL_INDEX = 1;
	public static final String SPEED_LEVEL_EASY = "easy";
	public static final String SPEED_LEVEL_NORMAL = "normal";
	public static final String SPEED_LEVEL_HARD = "hard";
	
	public static final int INIT_SPEED = 5;
	
	public static final String[] SETTING_ITEMS = {CLEAR_HIT_COUNT_LEVEL, SPEED_LEVEL};
	
	private Context mContext;
	private SharedPreferences mSettings;
	private SharedPreferences.Editor mSettingsEditor;
	
	public GameSettings(Context context){
		mContext = context;
                //PreferenceActivityから保存されるSharedPreferencesは
                //defaultのSharedPreferencesなのでSharePreferecesの値を読みに行く際にはその点、注意
		mSettings = PreferenceManager.getDefaultSharedPreferences(mContext);
		mSettingsEditor = mSettings.edit();
	}
	
	public String getClearHitCountLevel(){
		String clearHitCountLevel = mSettings.getString(CLEAR_HIT_COUNT_LEVEL, CLEAR_HIT_COUNT_LEVEL_NORMAL);
		Log.d(TAG, "clearHitCountLevel: " + clearHitCountLevel);
		return clearHitCountLevel;
	}
	
	public String getSpeedLevel(){
		String speedLevel = mSettings.getString(SPEED_LEVEL, SPEED_LEVEL_NORMAL); 
		Log.d(TAG, "speedLevel: " + speedLevel);
		return speedLevel;
	}
	
	/* PreferenceActivityでSharedPreferencesを設定するので必要なし
	public void setClearHitCountLevel(String clearHitCountLevel){
		mSettingsEditor.putString(CLEAR_HIT_COUNT_LEVEL, clearHitCountLevel);
	}
	
	public void setSpeedLevel(String speedLevel){
		mSettingsEditor.putString(CLEAR_HIT_COUNT_LEVEL, speedLevel);
	}
	
	public void commit(){
		mSettingsEditor.commit();
	}*/
}

SurfaceViewを拡張したclass

package com.android.practice;

import java.util.Random;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.TextView;
import android.widget.Toast;

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback{
	
	private static final String TAG = "MySurfaceView";
	
	public static final int HIT = 0;
	public static final int NO_HIT = 1;
	public static final int CLEAR = 2;
	
	private static final String THREAD_NAME = "DrawingThread";
	private MyDrawingThread mDrawingThread;
	private Context mContext;
	private Handler mHandler;
	private TextView mMsgTextView;
	
	public MySurfaceView(Context context) {
		super(context);
		mContext = context;
		getHolder().addCallback(this);
		mHandler = getMyHandler();
		mDrawingThread = new MyDrawingThread(THREAD_NAME, mHandler, this, mContext);
		setFocusable(true);
		requestFocus();
	}
	
	public MySurfaceView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mContext = context;
		getHolder().addCallback(this);
		mHandler = getMyHandler();
		mDrawingThread = new MyDrawingThread(THREAD_NAME, mHandler, this, mContext);
		setFocusable(true);
		requestFocus();
	}

	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
	}

	public void surfaceCreated(SurfaceHolder holder) {
		Log.d(TAG, "Thread: " + Thread.currentThread().getName() + " call surfaceCreated()");
		startDrawing();
	}
	
	private Handler getMyHandler(){
		return new Handler(){

			@Override
			public void handleMessage(Message msg) {
				Log.d(TAG, "handleMessage");
				
				if(mMsgTextView == null){
					Log.d(TAG, "mMsgTextView == null");
					return;
				}
				
				switch(msg.what){
				case HIT:
					mMsgTextView.setText(R.string.hit_msg);
					break;
				case NO_HIT:
					mMsgTextView.setText(R.string.no_hit_msg);
					break;
				case CLEAR:
					mMsgTextView.setText(R.string.clear_msg);
				default:
					break;
				}
			}
			
		};
	}

	public void surfaceDestroyed(SurfaceHolder holder) {
		Log.d(TAG, "Thread: " + Thread.currentThread().getName() + " call surfaceDestroyed()");
		stopDrawing();
	}
	
	private void startDrawing(){
		mDrawingThread.startDrawing();
	}
	
	private void stopDrawing(){
		mDrawingThread.stopDrawing();
		try{
			mDrawingThread.join();
		}catch(InterruptedException e){
		}
	}
	
	public void setTextView(TextView textView){
		mMsgTextView = textView;
	}
	
	public Rect getDrawingBoundary(){
		Rect drawingBoundary = new Rect();
		drawingBoundary.left = 0;
		drawingBoundary.top = 0;
		drawingBoundary.right = getWidth();
		drawingBoundary.bottom = getHeight() - mMsgTextView.getHeight();
		return drawingBoundary;
	}
	

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch(event.getAction() & MotionEvent.ACTION_MASK){
		case MotionEvent.ACTION_UP:
			Log.d(TAG, "TouchEvent: Action up");
			break;
		case MotionEvent.ACTION_DOWN:
			Log.d(TAG, "TouchEvent: Action down");
			mDrawingThread.checkBoundaryHit((int)event.getX(), (int)event.getY());
			break;
		case MotionEvent.ACTION_MOVE:
			Log.d(TAG, "TouchEvent: Action move");
			break;
		default:
			Log.d(TAG, "TouchEvent: Default");
			break;
		}
		return true;
	}
	
	
	private class MyDrawingThread extends Thread{
		
		private static final int LEFT = -1;
		private static final int RIGHT = 1;
		private static final int UP = -1;
		private static final int DOWN = 1;
		
		private volatile boolean mRun = false;
		
		private GameSettings mSettings;		
		private Handler mHandler;
		private SurfaceView mSurfaceView;
		private Context mContext;
		private Drawable mTargetImage;
		private Random mRandom;
		
		private int mSpeedX = GameSettings.INIT_SPEED;
		private int mSpeedY = GameSettings.INIT_SPEED;
		
		private int mHitCount = 0;
		private int mClearHitCount;
		private int mAdditionalSpeed;
		
		private Rect mDrawingBoundary;
		private Rect mTargetImageBoundary = new Rect();
		private int mTargetImageWidth;
		private int mTargetImageHeight;
		private int mDirectionX = RIGHT;
		private int mDirectionY = DOWN;
		
		private Paint mPaint = new Paint();
		
		public MyDrawingThread(String name, Handler handler, SurfaceView surfaceView, Context context){
			super(name);
			mHandler = handler;
			mSurfaceView = surfaceView;
			mContext = context;
			mTargetImage = context.getResources().getDrawable(R.drawable.icon);
			mSettings = new GameSettings(mContext);
			mRandom = new Random();
		}
		
		private void initUiEnv(SurfaceView surfaceView){
			mDrawingBoundary = ((MySurfaceView)mSurfaceView).getDrawingBoundary();
			
			
			mTargetImageWidth = mTargetImage.getIntrinsicWidth();
			mTargetImageHeight = mTargetImage.getIntrinsicHeight();
			
			mTargetImageBoundary.left = 0;
			mTargetImageBoundary.top = 0;
			mTargetImageBoundary.right = mTargetImageWidth;
			mTargetImageBoundary.bottom = mTargetImageHeight;
			
			mPaint.setColor(Color.WHITE);
		}
		
		private void initSettings(){
			String clearHitCountLevel = mSettings.getClearHitCountLevel();

			if(clearHitCountLevel.equals(GameSettings.CLEAR_HIT_COUNT_LEVEL_EASY)){
				mClearHitCount = 5;
			}else if(clearHitCountLevel.equals(GameSettings.CLEAR_HIT_COUNT_LEVEL_HARD)){
				mClearHitCount = 15;
			}else{
				mClearHitCount = 10;
			}
			
			String speedLevel = mSettings.getSpeedLevel();
			
			if(speedLevel.equals(GameSettings.SPEED_LEVEL_EASY)){
				mAdditionalSpeed = 5;
			}else if(speedLevel.equals(GameSettings.SPEED_LEVEL_HARD)){
				mAdditionalSpeed = 15;
			}else{
				mAdditionalSpeed = 10;
			}
			
			Toast.makeText(mContext, "hit count: " + mClearHitCount + " additional speed: " + mAdditionalSpeed, Toast.LENGTH_LONG).show();
		}
		
		public void startDrawing(){
			mRun = true;
			initSettings();
			initUiEnv(mSurfaceView);
			this.start();
		}
		
		public void stopDrawing(){
			mRun = false;
		}
		
		public synchronized void checkBoundaryHit(int x, int y){
			if(mTargetImageBoundary.contains(x, y)){
				
				mHitCount++;
				if(mHitCount >= mClearHitCount){
					mSpeedX = 0;
					mSpeedY = 0;
					mHandler.sendEmptyMessage(MySurfaceView.CLEAR);
				}else{
					if(mRandom.nextInt(2) == 0){
						mSpeedX = mSpeedX + mAdditionalSpeed;
					}else{
						mSpeedY = mSpeedY + mAdditionalSpeed;
					}
					mHandler.sendEmptyMessage(MySurfaceView.HIT);
				}
			}else{
				mHandler.sendEmptyMessage(MySurfaceView.NO_HIT);
			}
		}
		
		@Override
		public void run() {
			while(mRun){
				Canvas canvas = mSurfaceView.getHolder().lockCanvas();
				doDraw(canvas);
				mSurfaceView.getHolder().unlockCanvasAndPost(canvas);
			}
		}
		
		private void doDraw(Canvas canvas){
			canvas.drawColor(Color.BLACK);
			updateBoundary(); 
			mTargetImage.setBounds(mTargetImageBoundary);
			mTargetImage.draw(canvas);
		}
		
		private synchronized void updateBoundary(){
			int newLeft = 0;
			int newTop = 0;
			
			newLeft = mTargetImageBoundary.left + (mSpeedX * mDirectionX);
			
			if(mDirectionX == LEFT && newLeft <= mDrawingBoundary.left){
				mDirectionX = RIGHT;
				mTargetImageBoundary.left = mDrawingBoundary.left;
				mTargetImageBoundary.right = mTargetImageWidth;
			}else if(mDirectionX == RIGHT && (newLeft + mTargetImageWidth) >= mDrawingBoundary.right){
				mDirectionX = LEFT;
				mTargetImageBoundary.right = mDrawingBoundary.right;
				mTargetImageBoundary.left = mTargetImageBoundary.right - mTargetImageWidth;
			}else{
				mTargetImageBoundary.left = newLeft;
				mTargetImageBoundary.right = mTargetImageBoundary.left + mTargetImageWidth;
			}
			
			newTop = mTargetImageBoundary.top + (mSpeedY * mDirectionY);
			
			if(mDirectionY == UP && newTop <= mDrawingBoundary.top){
				mDirectionY = DOWN;
				mTargetImageBoundary.top = mDrawingBoundary.top;
				mTargetImageBoundary.bottom = mTargetImageHeight;
			}else if(mDirectionY == DOWN && (newTop + mTargetImageHeight) >= mDrawingBoundary.bottom){
				mDirectionY = UP;
				mTargetImageBoundary.bottom = mDrawingBoundary.bottom;
				mTargetImageBoundary.top = mTargetImageBoundary.bottom - mTargetImageHeight;
			}else{
				mTargetImageBoundary.top = newTop;
				mTargetImageBoundary.bottom = mTargetImageBoundary.top + mTargetImageHeight;
			}
		}
	}
}

PreferenceActivityを使うと簡単に設定画面が作成できました。
Activityを拡張したものが他にもあるのでそちらも用途により実装してみると簡単にやりたいことが実現出来るかもしれません。

HashTag #android, #java