marunomaruno-memo

marunomaruno-memo

[AppInventor] App Inventor のソースを取得する

2012年01月23日 | Android
[AppInventor] App Inventor のソースを取得する
================================================================================

1. ソースつぎのサイトから取得する。
app-inventor-releases
http://code.google.com/p/app-inventor-releases/

ここに、説明がついている。


2. つぎのサイトから Mercurial  をダウンロードして、設定する。

Mercurial downloads
http://mercurial.selenic.com/downloads/
Mercurial 2.0.2 Inno Setup installer - x86 Windows - does not require admin rights

インストーラーが、PATH の設定まで行ってくれる。

※Mercurial(マーキュリアル)は、クロスプラットフォームの分散型バージョン管理シ
ステム。 Mercurial はコマンドラインプログラムで、すべてのコマンドは hgで始まる。
(Mercurial - Wikipedia、http://ja.wikipedia.org/wiki/Mercurial より)

※Mercurial の使い方のチュートリアル
http://mercurial.selenic.com/wiki/JapaneseTutorial


3. ソースの取得

つぎのコマンドで取得する。
---
> hg clone https://code.google.com/p/app-inventor-releases/
destination directory: app-inventor-releases
requesting all changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1718 changes to 1718 files
updating to branch default
1718 files updated, 0 files merged, 0 files removed, 0 files unresolved
---

ソースは、カレント・ディレクトリー直下の以下のディレクトリーに入る。
	app-inventor-releases



[Android] グラフィックス (3) 円の軌跡で線を描く

2012年01月22日 | Android
グラフィックス (3) 円の軌跡で線を描く
================================================================================

次のサンプル。
・円の軌跡を使って線を描く
・クリア・ボタンをつける

「クリア・ボタンをつける」で、再び、レイアウト(res/layout/main.xml) を使うように
なる。


■ 円の軌跡で線を描く

円の軌跡を覚えておき、それをすべて描画することで、見た目としては線を描いているよ
うにする。もちろん、指を早く動かすと、単なる点点点にしかならない。

軌跡は、タッチして動かしているときのイベントからとられる座標を PointF オブジェク
トとして、List を使って保持しておく。

アクティビティや、レイアウト(res/layout/main.xml) は、前回と同じである。
グラフィックス (1)
http://blog.goo.ne.jp/marunomarunogoo/d/20111016


◆アクティビティ

円の軌跡になる点のリストをインスタンス変数にして、これを使って、イベントが起きる
たびにすべての点を再描画している。

□ GraphicsView.java
---
package jp.marunomaruno.android.graphics;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Paint.Style;
import android.util.AttributeSet;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

/**
 * 円の軌跡を描く。
 * @author marunomaruno
 * @see Graphics05Activity
 */
public class GraphicsView extends View {

    private Paint paint;
    private List<PointF> pointList;    // 円の軌跡になる点のリスト    // (1)
    private float radius;    // 円の半径

    public GraphicsView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize();
    }

    public GraphicsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    public GraphicsView(Context context) {
        super(context);
        initialize();
    }

    private void initialize() {
        // ペイントオブジェクトを設定する
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.BLUE);
        paint.setStyle(Style.FILL);

        // 軌跡を描画する初期値を設定する
        pointList = new ArrayList<PointF>();    // (2)
        radius = 10;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 格子を描画する
        drawGrid(canvas, 50);    

        // 軌跡を描画する
        for (PointF point : pointList) {    // (3)
            canvas.drawCircle(point.x, point.y, radius, paint);    // (4)
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        
        case MotionEvent.ACTION_DOWN:    // 指をタッチした    // (5)
        case MotionEvent.ACTION_MOVE:    // 指を動かしている    // (6)
            pointList.add(new PointF(event.getX(), event.getY()));    // (7)
            break;

        case MotionEvent.ACTION_UP:        // 指を離した
            assert true;    // 何もしない
            break;

        default:
            assert true;    // 何もしない
            break;
        }
        
        invalidate();

        return true;
    }

    /**
     * 画面に格子を描画する。
     * @param canvas
     * @param interval 格子を描く間隔
     */
    private void drawGrid(final Canvas canvas, int interval) {
        // 画面のサイズを取得する
        WindowManager manager = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        int width = display.getWidth();
        int height = display.getHeight();

        // 格子を描画する
        Paint paint = new Paint();
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth(1);
        for (int i = 0; i < Math.max(width, height); i += interval) {
            canvas.drawText(Integer.toString(i), i, paint.getTextSize(), paint);
            canvas.drawLine(i, 0, i, height, paint);
            canvas.drawText(Integer.toString(i), 0, i, paint);
            canvas.drawLine(0, i, width, i, paint);
        }
    }
}
---

(1) 円の軌跡になる点のリスト

点の座標は、PointF クラスのオブジェクトとして持つ。

    private List<PointF> pointList;    // 円の軌跡になる点のリスト    // (1)


・PointF クラス

java.lang.Object
   +     android.graphics.PointF


・主なフィールド
---
public float  x
public float  y
---

public なので、直接、このインスタンス変数を使うことができる。
コードとしては、かなりすっきりした見た目になる。


・コンストラクター
---
PointF()
PointF(float x, float y)
PointF(Point p)
---


・主なメソッド
---
final  boolean  equals(float x, float y)
final  float    length()                    原点 (0, 0) からの距離
static float    length(float x, float y)    (0, 0) - (x, y) の距離
final  void     negate()
final  void     offset(float dx, float dy)  (cx, cy) - (dx, dy) の距離
final  void     set(float x, float y)
final  void     set(PointF p)
---


(2) 軌跡を描画する初期値を設定する

空のリストを準備する。

    pointList = new ArrayList<PointF>();    // (2)


(3)(4) 軌跡を描画する

座標リストから座標をひとつづつ取り出して、それをもとに描画する。

    for (PointF point : pointList) {    // (3)
        canvas.drawCircle(point.x, point.y, radius, paint);    // (4)
    }


(5)(6)(7) ハンドラーの処理

指がタッチしたときと、動いているときのイベントの座標をリストに追加する。

    case MotionEvent.ACTION_DOWN:    // 指をタッチした    // (5)
    case MotionEvent.ACTION_MOVE:    // 指を動かしている    // (6)
        pointList.add(new PointF(event.getX(), event.getY()));    // (7)



■ クリアボタンをつける

線を描くだけではなく、描いた線をすべて消せるように、クリアボタンをつける。
ボタンを描画域(カスタム・ビュー)の上につけることもできるが、描画域とは別にボタン
があったほうが使い勝手はよい。
したがって、今までのサンプルとは違い、res/layout/main.xml で、クリアボタンとカス
タムビューで画面をレイアウトする。

res/layout/main.xml で今まで LinearLayout や、Button などといった要素を書いてき
たが、これも Android が用意している View クラスのサブクラスである。
自分で作っているカスタムビューのクラスも、View クラスのサブクラスとして、
res/layout/main.xml で、Button などの標準で用意されているビューのコンポーネント
と同じように使える。ただし、標準ではないので、要素名は完全修飾名を使う。


◆アクティビティ

□ Graphics051Activity
---
package jp.marunomaruno.android.graphics;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;

public class Graphics051Activity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);    // (1)
    }

    /**
     * 画面ををクリアする。
     * @param view
     */
    public void onClickClearButton(View view) {
        GraphicsView graphicsView = (GraphicsView) findViewById(R.id.graphicsView);
        graphicsView.clear();    // (2)
    }
}
---

(1) レイアウトの割り当て

    setContentView(R.layout.main);    // (1)


(2) 画面をクリアする

    graphicsView.clear();    // (2)


◆カスタムビュー

クリアするメソッド clear() を追加しただけ。

□ GraphicsView.java
---
package jp.marunomaruno.android.graphics;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Paint.Style;
import android.util.AttributeSet;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

/**
 * 円の軌跡を描く。クリアボタン付き。
 * @author marunomaruno
 * @see Graphics051Activity
 */
public class GraphicsView extends View {

    private Paint paint;
    private List<PointF> pointList;    // 円の軌跡になる点のリスト
    private float radius;    // 円の半径

    public GraphicsView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize();
    }

    public GraphicsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    public GraphicsView(Context context) {
        super(context);
        initialize();
    }

    private void initialize() {
        // ペイントオブジェクトを設定する
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.BLUE);
        paint.setStyle(Style.FILL);

        // 軌跡を描画する初期値を設定する
        pointList = new ArrayList<PointF>();
        radius = 10;

    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 格子を描画する
        drawGrid(canvas, 50);    

        // 軌跡を描画する
        for (PointF point : pointList) {
            canvas.drawCircle(point.x, point.y, radius, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        
        case MotionEvent.ACTION_DOWN:    // 指をタッチした
        case MotionEvent.ACTION_MOVE:    // 指を動かしている
            pointList.add(new PointF(event.getX(), event.getY()));
            break;

        case MotionEvent.ACTION_UP:        // 指を離した
            assert true;    // 何もしない
            break;

        default:
            assert true;    // 何もしない
            break;
        }
        
        invalidate();

        return true;
    }

    /**
     * 画面に格子を描画する。
     * @param canvas
     * @param interval 格子を描く間隔
     */
    private void drawGrid(final Canvas canvas, int interval) {
        // 画面のサイズを取得する
        WindowManager manager = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        int width = display.getWidth();
        int height = display.getHeight();

        // 格子を描画する
        Paint paint = new Paint();
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth(1);
        for (int i = 0; i < Math.max(width, height); i += interval) {
            canvas.drawText(Integer.toString(i), i, paint.getTextSize(), paint);
            canvas.drawLine(i, 0, i, height, paint);
            canvas.drawText(Integer.toString(i), 0, i, paint);
            canvas.drawLine(0, i, width, i, paint);
        }
    }

    /**
     * 軌跡をクリアする。
     */
    public void clear() {    // (1)
        initialize();    // (2)
        invalidate();    // (3)
    }
}
---

(1)-(3) 軌跡をクリアする

    public void clear() {    // (1)

座標のリストをクリアするので、初期値かメソッドを呼ぶ。

    initialize();    // (2)

画面を無効にして描画しなおす。

    invalidate();    // (3)


◆レイアウト

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/clearButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:onClick="onClickClearButton"
        android:text="@string/clearButton" />

    <jp.marunomaruno.android.graphics.GraphicsView
        android:id="@+id/graphicsView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />    
</LinearLayout>
---

(1) カスタムビューの指定

要素名は、カスタム・ビュー・クラスの完全修飾名を記す。

    <jp.marunomaruno.android.graphics.GraphicsView
        android:id="@+id/graphicsView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />    


◆文字列

□ res/values/strings.xml
---

<resources>
    <string name="app_name">Graphics051</string>
    <string name="clearButton">クリア</string>
</resources>
---

                                                                            以上


[Android] グラフィックス (2) 指を追うように円を動かす - タッチ・イベント

2012年01月19日 | Android
[Android] グラフィックス (2) 指を追うように円を動かす - タッチ・イベント
================================================================================

画面に描いた円が、タッチした指を追って動くように描画するサンプル。
タッチしたことに対して、何か処理を行うので、タッチ・イベントに対するハンドラーを
つくる。ハンドラーは、View クラスの onTouchEvent() メソッドをオーバーライドする。

アクティビティや、レイアウト(res/layout/main.xml) は、前回と同じである。
グラフィックス (1)
http://blog.goo.ne.jp/marunomarunogoo/d/20111016


◆ 描画関係のクラス

□ GraphicsView.java
---
package jp.marunomaruno.android.graphics;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.util.AttributeSet;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;

/**
 * 指を追うように円動かす。
 * @author marunomaruno
 * @see Graphics04Activity
 */
public class GraphicsView extends View {

    private Paint paint;
    private float cx;    // 図形を描画する X 座標    // (1)
    private float cy;    // 図形を描画する Y 座標    // (2)
    private float radius;    // 円の半径    // (3)

    public GraphicsView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize();
    }

    public GraphicsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize();
    }

    public GraphicsView(Context context) {
        super(context);
        initialize();
    }

    private void initialize() {
        // ペイントオブジェクトを設定する
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.BLUE);    // (4)
        paint.setStyle(Style.FILL);    // (5)

        // 丸を描画する初期値を設定する
        cx = 100;
        cy = 200;
        radius = 50;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 格子を描画する
        drawGrid(canvas, 50);    

        // 円を描画する
        canvas.drawCircle(cx, cy, radius, paint);    // (6)
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {    // (7)
        switch (event.getAction()) {
        
        case MotionEvent.ACTION_DOWN:    // 指をタッチした    // (8)
            assert true;    // 何もしない
            break;

        case MotionEvent.ACTION_MOVE:    // 指を動かしている    // (9)
            cx = event.getX();    // (10)
            cy = event.getY();    // (11)
            break;

        case MotionEvent.ACTION_UP:        // 指を離した    // (12)
            assert true;    // 何もしない
            break;

        default:
            assert true;    // 何もしない
            break;
        }
        
        invalidate();    // (13)

        return true;    // (14)
    }

    /**
     * 画面に格子を描画する。
     * @param canvas
     * @param interval 格子を描く間隔
     */
    private void drawGrid(final Canvas canvas, int interval) {
        // 画面のサイズを取得する
        WindowManager manager = (WindowManager) getContext().getSystemService(
                Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        int width = display.getWidth();
        int height = display.getHeight();

        // 格子を描画する
        Paint paint = new Paint();        // (15)
        paint.setColor(Color.WHITE);
        paint.setStrokeWidth(1);
        for (int i = 0; i < Math.max(width, height); i += interval) {
            canvas.drawText(Integer.toString(i), i, paint.getTextSize(), paint);
            canvas.drawLine(i, 0, i, height, paint);
            canvas.drawText(Integer.toString(i), 0, i, paint);
            canvas.drawLine(0, i, width, i, paint);
        }
    }
}
---

(1)(2) 図形を描画する座標の保持

onTouchEvent() で取得した座標を、onDraw() メソッドで使うので、インスタンス変数に
しておく。

    private float cx;    // 図形を描画する X 座標    // (1)
    private float cy;    // 図形を描画する Y 座標    // (2)


(3) 円の半径

    private float radius;    // 円の半径    // (3)

ただし、これはプログラム中で変更しないのであれば、定数でもよい。


(4)(5) ペイントオブジェクトを設定する

今回は、色を青、円の中は塗りつぶすことにする。

    paint.setColor(Color.BLUE);    // (4)
    paint.setStyle(Style.FILL);    // (5)


(6) 円を描画する

drawCircle() メソッドを使って円を描く。

    canvas.drawCircle(cx, cy, radius, paint);    // (6)


(7) タッチイベントのハンドラー・メソッド

View クラスの次のメソッドをオーバーライドする。

    public boolean onTouchEvent(MotionEvent event) {    // (7)

引数は MotionEvent オブジェクト。
返り値は、イベントの処理が終わった(true)か終わっていないか(false)。


・MotionEvent クラス

指やマウス、ペン、トラックボールの動きのイベント。

java.lang.Object
   +     android.view.InputEvent
         +     android.view.MotionEvent

・Action 関係で、指の動き関係(と思われる)の定数
---
int  ACTION_DOWN  タッチしたとき(指をつけたとき)
int  ACTION_MOVE  指を動かしているとき(ACTION_DOWN と ACTION_UP の間)
int  ACTION_UP    指を離したとき
---

・主なメソッド
---
final int    getAction()
final int    getPointerCount()                
final int    getPointerId(int pointerIndex)
final float  getX(int pointerIndex)
final float  getY(int pointerIndex)
final float  getY()
---


(8)(9)(12) アクションによる処理

上記 getAction() メソッドにより、どのアクションかを取得した後、アクションごとに
そのアクションに対する処理をケースわけしていく。

    case MotionEvent.ACTION_DOWN:    // 指をタッチした    // (8)
    case MotionEvent.ACTION_MOVE:    // 指を動かしている    // (9)
    case MotionEvent.ACTION_UP:        // 指を離した    // (12)


(10)(11) 指を動かしているときに、その位置の座標を取得する

タッチしている位置は、event オブジェクトの getX()、getY() メソッドを使って取得す
る。

    cx = event.getX();    // (10)
    cy = event.getY();    // (11)


(13) 再描画

ビュー全体を一度無効にすることで、再描画する。

    invalidate();    // (13)

・View クラスのオーバーロードされている invalidate() メソッド
---
void  invalidate(Rect dirty)
void  invalidate(int l, int t, int r, int b)
void  invalidate()
void  invalidateDrawable(Drawable drawable)
---


(14) アクションは処理済とする

    return true;    // (14)

この返り値が false のままだと、未処理となるので、つぎのイベントを拾えない。
今回は、MotionEvent を拾うハンドラーは、このメソッドなので、true にしておく。


(15) 格子描画用のペイント・オブジェクト

格子に使う使う色や線の太さなど、描きたい図形と違う場合があるので、格子用のペイン
ト・オブジェクトを作っておく。

    Paint paint = new Paint();        // (15)



[Android][SQLite] DBViewer プラグインのインストールと設定

2012年01月17日 | Android
[Android][SQLite] DBViewer プラグインのインストールと設定
================================================================================

■ 1. 必要なファイルをダウンロード

・SQLiteJDBC ドライバーのダウンロード

SQLiteJDBC
http://www.xerial.org/trac/Xerial/wiki/SQLiteJDBC
http://www.xerial.org/maven/repository/artifact/org/xerial/sqlite-jdbc/3.7.2/
から、
sqlite-jdbc-3.7.2.jar
をダウンロード

これを適当なフォルダに入れておく。

たとえば、
%JAVA_HOME%¥jre¥lib¥ext
に入れておくと、他の Java アプリケーションなどでも利用できる。
%JAVA_HOME% は、JavaSE をインストールしたホームディレクトリー。


・Eclipse プラグインの DBViewer をダウンロード
DBViewer Plugin for Eclipse 開発プロジェクト
http://www.ne.jp/asahi/zigen/home/plugin/dbviewer/about_jp.html
http://sourceforge.jp/projects/dbviewer/releases/49262
から、
zigen.plugin.db_1.2.2.v20101009.jar
をダウンロード

これは、
%ECLIPSE_HOME%¥plugins
に入れておく。
%ECLIPSE_HOME%は、Eclipse をインストールしたホームディレクトリー。


■ 2. Eclipse での設定


□ データベースを定義

・つぎの操作で、DBツリー・ビューを表示する
[Windows] - [Show View] - [Other...] - [DBViewer Plugin] - [DBツリー・ビュー]


・データベースの定義

[DBViewer Plugin]を右クリックして [登録]

以下を設定
---
データベース定義名    任意に指定(プロジェクト名とデータベース名を組み合わせると
                      わかりやすい)

JDBCドライバーのパス  [ファイルの追加]で、ダウンロードした JAR ファイルを指定
---

上記を設定したら [次へ]

以下を設定
---
JDBCドライバー    org.sqlight.JDBC ... 自動で設定されている

接続文字列        jdbc:sqlite:【データベースのパス】
---

DDMSのファイル・エクスプローラーからダウンロードしたものは、デフォルトで
[C:¥Documents and Settings¥【ユーザー名】]
に入るので、ここのパスをそのまま接続文字列の[データベースのパス]にする。

たとえば、
    jdbc:sqlite:C:¥Documents and Settings¥user¥SampleDB


■ 参考 URL

インストールについては以下を参照

SQLiteデータベースの中身を見る[DBViewer]
http://www.andro-dev.com/201104/sqlitedbviewer.php



[Android] センサー (3) 方位

2012年01月14日 | Android
センサー (3) 方位
================================================================================

■ 方位を取得する

方位は、それ専用のセンサーがあるのではなく、加速度センサーと磁気センサーの値から
算出する。この 2 つのセンサーを使うこと以外は、いままでのサンプルと同じである。

Sensor.TYPE_ORIENTATION は、非推奨になっているので、これは使わない。


◆ アクティビティのクラス。

□ OrientationSensor01Activity.java
---
package jp.marunomaruno.android.orientationsensor;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;
import jp.marunomaruno.android.orientationsensor.R;

/**
 * 方位を表示する。
 * @author marunomaruno
 * @version 1.0, 2011-12-01
 */
public class OrientationSensor01Activity extends Activity {

    // TextView
    private TextView orientationTextView; // 方位センサーの表示域

    private SensorManager sensorManager;
    private SensorEventListener sensorEventListener; 
                    // onPause()が呼ばれたときに、すべてのリスナーを解除するため

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        orientationTextView = (TextView) findViewById(R.id.sensor_value);
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    }

    @Override
    protected void onResume() {
        super.onResume();

        sensorEventListener = new OrientationSensorListener(orientationTextView); // (1)

        // 加速度センサーを取得して登録する
        for (Sensor sensor : sensorManager
                .getSensorList(Sensor.TYPE_ACCELEROMETER)) {
            sensorManager.registerListener(sensorEventListener, sensor,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }

        // 磁気センサーを取得して登録する
        for (Sensor sensor : sensorManager
                .getSensorList(Sensor.TYPE_MAGNETIC_FIELD)) {
            sensorManager.registerListener(sensorEventListener, sensor,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        // すべてのリスナーを解除する
        sensorManager.unregisterListener(sensorEventListener);
    }
}
---

(1) 方位センサーのリスナー・オブジェクトを生成する

    sensorEventListener = new OrientationSensorListener(orientationTextView); // (1)

そして、このオブジェクトを、加速度センサー、磁気センサーに登録しておく。


◆ センサーのリスナーのクラス。

センサーに対するリスナーのクラス。
これは、加速度センサーと磁気センサーの 2 つのセンサーに登録される。
方位は、この 2 つのセンサーの値を基に取得する。



□ AcceleraterSensorListener.java
---
package jp.marunomaruno.android.orientationsensor;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.widget.TextView;
import jp.marunomaruno.android.orientationsensor.R;

/**
 * 方位センサーのリスナー
 * @author maruno
 * @version 1.0, 2011-12-01
 * @since 1.0
 */
public class OrientationSensorListener implements SensorEventListener {

    private TextView textView; // 表示域
    private float[] accelerometerValues; // 加速度センサーの値    // (1)
    private float[] magneticFieldValues; // 磁気センサーの値    // (2)

    public OrientationSensorListener(TextView textView) {
        this.textView = textView;
    }

    @Override
    public void onSensorChanged(SensorEvent event) {

        float[] orientationValues = new float[3]; // 方位の値    // (3)

        switch (event.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER: // 加速度センサーが変化したとき
            accelerometerValues = clone(event.values);    // (4)
            break;

        case Sensor.TYPE_MAGNETIC_FIELD: // 磁気センサーが変化したとき
            magneticFieldValues = clone(event.values);    // (5)
            break;
        }

        if (accelerometerValues != null && magneticFieldValues != null) {

            float[] inRotationMatrix = new float[16]; // 入力用の回転行列  // (6)
            float[] outRotationMatrix = new float[16]; // 出力用の回転行列 // (7)

            SensorManager.getRotationMatrix(inRotationMatrix, null,
                    accelerometerValues, magneticFieldValues);           // (8)
            SensorManager.remapCoordinateSystem(inRotationMatrix,
                    SensorManager.AXIS_X, SensorManager.AXIS_Z,
                    outRotationMatrix);                                  // (9)
            SensorManager.getOrientation(outRotationMatrix, orientationValues); // (10)

            // TextViewに表示する
            textView.setText("");

            // ラジアンを表示
            textView.append(String
                    .format(textView.getResources().getString(
                            R.string.sensor_value_format), textView
                            .getResources().getString(R.string.orientation),
                            orientationValues[0], orientationValues[1],
                            orientationValues[2], textView.getResources()
                                    .getString(R.string.OrientationUnit)));
            textView.append("¥n");

            // 度を表示
            textView.append(String.format(textView.getResources().getString(
                    R.string.sensor_value_format), textView.getResources()
                    .getString(R.string.orientation),
                    toOrientationDegrees(orientationValues[0]),
                    toOrientationDegrees(orientationValues[1]),
                    toOrientationDegrees(orientationValues[2]), textView
                            .getResources()
                            .getString(R.string.OrientationUnit2)));
            textView.append("¥n");

            // 方位を表示
            textView.append(String.format(textView.getResources().getString(
                    R.string.orientation_value_format),
                    toOrientationString(orientationValues[0]),
                    toOrientationDegrees(orientationValues[0]), textView
                            .getResources()
                            .getString(R.string.OrientationUnit2)));
            textView.append("¥n");
        }
    }

    /**
     * 配列の深いコピーを行う。
     * ※Android2.2 の Arrays クラスには copyOf() メソッドがない。
     * @param source ソースの配列
     * @return 深いコピーを行った結果
     */
    private float[] clone(float[] source) {                        // (11)
        float[] target = new float[source.length];
        for (int i = 0; i < target.length; i++) {
            target[i] = source[i];
        }
        return target;
//        return Arrays.copyOf(sourse, sourse.length);    // 本来はこれでOK
    }

    /**
     * 方位の角度にする。方位の角度は、0以上360未満。
     * @param rad ラジアン。ただし、-π以上π未満。
     * @return 方位の角度
     */
    private float toOrientationDegrees(float rad) {                // (12)

        return (float) (rad >= 0 ? Math.toDegrees(rad) : 360 + Math
                .toDegrees(rad));    // (13)
    }

    /**
     * 方位をあらわす文字列を取得する。 
     *         北 0 rad 
     *         東 π/2 rad 
     *         南 π rad 
     *         西 -π/2 rad
     * @param azimuth 方位
     * @return 方位をあらわす文字列
     */
    private String toOrientationString(float azimuth) {            // (14)
        double[] ORIENTATION_RANGE = {
                -(Math.PI * 3 / 4), // 南
                -(Math.PI * 1 / 4), // 西
                +(Math.PI * 1 / 4), // 北
                +(Math.PI * 3 / 4), // 東
                // Math.PI, // 南
        };    // (15)

        for (int i = 0; i < ORIENTATION_RANGE.length; i++) {
            if (azimuth < ORIENTATION_RANGE[i]) {
                return textView.getResources().getStringArray(
                        R.array.orientationNames)[i];    // (16)
            }
        }

        return textView.getResources().getStringArray(R.array.orientationNames)[0];
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        assert true; // 何もしない
    }
}
---

(1)(2) 加速度センサー・磁気センサーの値を保持

それぞれ、3 次元の値なので、配列で保持する。

    private float[] accelerometerValues; // 加速度センサーの値    // (1)
    private float[] magneticFieldValues; // 磁気センサーの値    // (2)


(3) 方位の値

方位も、3 次元の値になるので、これを確保する変数を用意する。

    float[] orientationValues = new float[3]; // 方位の値    // (3)


(4)(5)(6)(7) 加速度センサー・磁気センサーの値から方位を取得するための準備

計算に使うので、配列の深いコピーで、値をコピーする。

    accelerometerValues = clone(event.values);    // (4)
    magneticFieldValues = clone(event.values);    // (5)

※Android2.2 では、深いコピーを行う Arrays.copyOf() メソッドがないので、自分で実
装する。本来は、
    accelerometerValues = Arrays.copyOf(event.values, event.values.length); // (4)
    magneticFieldValues = Arrays.copyOf(event.values, event.values.length); // (5)
でよい。

4x4 の回転行列を用意する。実際には、要素数 16 の 1 次元配列で作る。

    float[] inRotationMatrix = new float[16]; // 入力用の回転行列    // (6)
    float[] outRotationMatrix = new float[16]; // 出力用の回転行列    // (7)


(8)(9)(10) 加速度センサー・磁気センサーの値から方位を取得する

この一連の操作によって、accelerometerValues、magneticFieldValues の値は、最終的
に orientationValues として方位が取得できる。

加速度センサー・磁気センサーの値から、回転行列 inRotationMatrix を取得する。

    SensorManager.getRotationMatrix(inRotationMatrix, null,
            accelerometerValues, magneticFieldValues);           // (8)

座標系を回転行列 inRotationMatrix を使って変換し、outRotationMatrix に入れる。
今回は、画面をみるときに、たて画面で、垂直に近い形(手前に傾いている)で見ること
を想定。
画面の傾き具合は、Display.getRotation() を使って取得するが、この部分はかなり複雑
になるので、今回はこのまま。
※他のサイトでは、引数(..., SensorManager.AXIS_X, SensorManager.AXIS_Y, ...) が
よいともあるが、わたしの実機では下記の引数の方がよい感じ。

    SensorManager.remapCoordinateSystem(inRotationMatrix,
            SensorManager.AXIS_X, SensorManager.AXIS_Z,
            outRotationMatrix);                                  // (9)

回転行列 outRotationMatrix からデバイスの向き orientationValues を取得する。

    SensorManager.getOrientation(outRotationMatrix, orientationValues);  // (10)

※回転行列などの詳細は現在調査中。


・SensorManager クラスの static な取得系のメソッド
---
static float    getAltitude(float p0, float p)    
                大気圧と海面の気圧から高度(m)を取得

static void     getAngleChange(float[] angleChange, float[] R, float[] prevR)  
                2 つの回転行列の間の角度を取得

static float    getInclination(float[] I)    
                getRotationMatrix() で取得した傾き行列から傾きを取得

static float[]  getOrientation(float[] R, float[] values)    
                回転行列からデバイスの向きを取得

static void     getQuaternionFromVector(float[] Q, float[] rv)    
                回転ベクトルから 4 元数を取得(ヘルパー)

static boolean  getRotationMatrix(float[] R, float[] I, float[] gravity, 
                                                        float[] geomagnetic)   
                回転行列を取得

static void     getRotationMatrixFromVector(float[] R, float[] rotationVector) 
                回転ベクトルから回転行列を取得(ヘルパー)

static boolean  remapCoordinateSystem(float[] inR, int X, int Y, float[] outR) 
                座標系を回転行列を使って変換
---
※ただし、使い方(訳も)についてはまだ、不明な部分が大半。


・remapCoordinateSystem() で使う定数
---
int     AXIS_MINUS_X
int     AXIS_MINUS_Y
int     AXIS_MINUS_Z
int     AXIS_X
int     AXIS_Y
int     AXIS_Z
---


(11) 配列の深いコピーを行う

    private float[] clone(float[] source) {                        // (11)

※Android2.2 では、まだ、これを実現する Arrays.copyOf() メソッドがないので、自分
で実装する。


(12)(13) 方位の角度にする。方位の角度は、0以上360未満。

    private float toOrientationDegrees(float rad) {                // (12)

rad は、-π ~ +πの範囲なので、これを度に変換し、値の範囲が 0 ~ 360 になるよう
に調整する。

    return (float) (rad >= 0 ? Math.toDegrees(rad) : 360 + Math
            .toDegrees(rad));    // (13)


(14)(15)(16) 方位をあらわす文字列を取得する。 

    private String toOrientationString(float azimuth) {            // (14)

真北、真東、真南、真西がそれぞれ 0, π/2, -π, -π/2 なので、北、東、南、西のそ
れぞれの範囲は、以下のようになる。
    北    -π/4 ~ π/4
    東     π/4 ~  3π/4
    南    -π   ~ -3π/4, 3π/4 ~ π
    西    -π/4 ~ π/4

    double[] ORIENTATION_RANGE = {
            -(Math.PI * 3 / 4), // 南
            -(Math.PI * 1 / 4), // 西
            +(Math.PI * 1 / 4), // 北
            +(Math.PI * 3 / 4), // 東
            // Math.PI, // 南
    };    // (15)

この範囲で、配列を作って、文字を割り当てる。

    return textView.getResources().getStringArray(
            R.array.orientationNames)[i];    // (16)

リソースでは、string-array 要素を使って、配列として、東西南北の文字列を持つよう
にしている。
    <string-array name="orientationNames">
        <item>@string/south</item>
        <item>@string/west</item>
        <item>@string/north</item>
        <item>@string/east</item>
    </string-array>

これは、getStringArray() メソッドで、ここで記述した順番の String[] 型の配列で値
が返ってくる。


■ レイアウト

センサー(1) のサンプルと同じ。

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    
<!-- センサー値 -->    
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/orientation"
    android:id="@+id/sensor_value"
    />
    
</LinearLayout>
---


■ 文字列の定数

プログラム中で配列を使っているので、それに対する string-array 要素を設定している
ファイル arrays.xml も準備する。string.xml の中に入れてもよいが、配列として文字
列を 2 次的に利用しているので、別ファイルにした。

□ res/values/string.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">OrientationSensor01</string>
    <string name="orientation">方位: (方位角(azimuth), ピッチ(Pitch), 回転(Roll))</string>
    <string name="OrientationUnit">ラジアン</string>
    <string name="OrientationUnit2">度</string>
    <string name="sensor_value_format">%1$s = (%2$.1f, %3$.1f, %4$.1f) (%5$s)</string>
    <string name="orientation_value_format">方位: %1$s (%2$.1f %3$s)</string>
    <string name="east">東(45 ~ 135)</string>
    <string name="west">西(225 ~ 315)</string>
    <string name="south">南(135 ~ 225)</string>
    <string name="north">北(0 ~ 45, 315 ~ 360)</string>
</resources>
---

□ res/values/arrays.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="orientationNames">
        <item>@string/south</item>
        <item>@string/west</item>
        <item>@string/north</item>
        <item>@string/east</item>
    </string-array>
</resources>
---

東西南北の文字列を配列で持っておく。
プログラム中でも配列を使うので、リソースも、string-array 要素を使って、配列とし
て管理する。



位置情報 (4) / ファイル・システム (2) 外部メディアに位置情報を書き込む

2012年01月11日 | Android
位置情報 (4) / ファイル・システム (2) 外部メディアに位置情報を書き込む
================================================================================

このアプリケーションは、つぎつぎと変化する位置情報を、SD カードのファイルに書き
込む。GpsLogger01 は、メモリー内のファイルであったが、これは SD カードを使うのが
違う点である。 また、利用者の指定によって、ファイルの内容を表示したり、削除した
りするのは、GpsLogger01 と同じ。

コード
---
GpsLogger01Activity.java  アクティビティ
GpsLocationListener.java  ロケーションのリスナー
TextViewPrintStream.java  System.out オブジェクトの置き換え(SyystemOut01 と同じ)
LogDao.java               ログの入出力を行うユーティリティのクラス
---


◆ アクティビティのクラス

採ったログを表示するための「ログ表示」ボタン、ログファイルをクリアする「ログクリ
ア」ボタンが追加になっているので、これに対するハンドラー・メソッド
    onClickLogPrintButton()
    onClickLogClearButton()
を追加している。メソッドは、単に LogDao のメソッドを呼び出しているだけである。

ファイル名は、先のサンプル GpsLogger01 は、固定だったが、これは、ログを採る開始
日時がファイル名の一部に入る。

ログファイルの扱いは、LogDao クラスなので、このアクティビティ・クラスはほとんど
変更がない。ログファイルにタイムスタンプが入っているので、それを取得するコードが
入った程度である。タイムスタンプに対するゲッター・メソッドが追加になっている。


□ Gps02Activity.java
---
package jp.marunomaruno.android.gpslogger;

import java.io.FileNotFoundException;
import java.io.IOException;

import android.app.Activity;
import android.location.Criteria;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

/**
 * @author marunomaruno
 * @version 1.0, 2011-12-14
 * @since 1.0
 */
public class GpsLogger02Activity extends Activity {

    private boolean isRunning = false;    // 現在起動中かどうか
    private static final String IS_RUNNING = "isRunning";

    private long timestamp;    // 開始したときのタイムスタンプ        // (1)
    private static final String TIMESTAMP = "timestamp";

    private LocationManager locationManager;
    private LocationListener sensorEventListener; 
                    // onPause()が呼ばれたときに、すべてのリスナーを解除するため
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d(getString(R.string.logTag), String.format("onCreate() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // 標準出力を置き換え(標準出力(LogCat)+テキストビュー)
        TextView sysout = (TextView) findViewById(R.id.sysout);
        System.setOut(new TextViewPrintStream(System.out, sysout));
        System.setErr(new TextViewPrintStream(System.err, sysout));

        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
    }

    @Override
    protected void onResume() {
        Log.d(getString(R.string.logTag), String.format("onResume() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onResume();

        Button startStopButton = (Button) findViewById(R.id.startStopButton);

        Log.d(getString(R.string.logTag), "起動中 = " + isRunning);
        
        if (isRunning) {    // 起動中のとき、終了ボタンを表示する
            startStopButton.setText(R.string.stopButton);

        } else {
            startStopButton.setText(R.string.startButton);

        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        Log.d(getString(R.string.logTag), String.format("onRestoreInstanceState() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onRestoreInstanceState(savedInstanceState);
        isRunning = savedInstanceState.getBoolean(IS_RUNNING);
        timestamp = savedInstanceState.getLong(TIMESTAMP);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        Log.d(getString(R.string.logTag), String.format("onSaveInstanceState() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onSaveInstanceState(outState);
        outState.putBoolean(IS_RUNNING, isRunning);
        outState.putLong(TIMESTAMP, timestamp);
    }

    /**
     * 開始/終了ボタン押下時
     * @param view
     */
    public void onClickStartStopButton(View view) {
        Log.d(getString(R.string.logTag), String.format("onClickStartButton() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        Button startStopButton = (Button) view;
        
        if (isRunning) {    // 起動中のとき、とめる
            // すべてのリスナーを解除する
            removeListeners();

            // 開始ボタンにする
            startStopButton.setText(R.string.startButton);

            System.out.println(getString(R.string.stopButton));
            System.out.println(String.format(
                    getString(R.string.sensor_value_format_for_timestamp),
                    getString(R.string.startButton), timestamp));
            System.out.println(String.format(
                    getString(R.string.sensor_value_format_for_timestamp),
                    getString(R.string.stopButton), System
                            .currentTimeMillis()));

        } else {
            // 起動していないとき、
            // GPSロケーションを取得して登録する
            setProviderAndListener();
            
            // 停止ボタンにする
            startStopButton.setText(R.string.stopButton);
            timestamp = System.currentTimeMillis();    
                                        // タイムスタンプを取得する    // (2)
            try {
                System.out.printf(getString(R.string.startMessageFormat),
                        LogDao.getFile(this));
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println();
        }
        isRunning = !isRunning;    // 起動中状態を逆にする
    }

    private void setProviderAndListener() {
        // プロバイダーを取得する条件を作成する
        Criteria fineAndLowPower = new Criteria();
        fineAndLowPower.setAccuracy(Criteria.ACCURACY_FINE);
        fineAndLowPower.setPowerRequirement(Criteria.POWER_LOW);
        
        // プロバイダーを取得する
        String provider = locationManager.getBestProvider(fineAndLowPower, true);
        Log.d(getString(R.string.logTag), "provider = "
                + provider);
        TextView providerNameTextView = (TextView) findViewById(R.id.provider_name);
        providerNameTextView.setText(provider);
        
        // プロバイダーがみつからなければ、NETWORK_PROVIDER を設定しておく
        if (provider == null) {
            provider = LocationManager.NETWORK_PROVIDER;
            providerNameTextView.setText(R.string.no_provider);
        }
        
        // 位置情報のリスナーを取得して登録する
        sensorEventListener = GpsLocationListener.getInstance(this);
        locationManager.requestLocationUpdates(
                provider, 
                0, 
                0, 
                sensorEventListener);
    }

    /**
     * ログをクリアする。
     * @param view
     */
    public void onClickLogClearButton(View view) {
        try {
            LogDao.delete(this);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * ログを表示する。
     * @param view
     */
    public void onClickLogPrintButton(View view) {
        try {
            LogDao.print(this);
        } catch (FileNotFoundException e) {
            System.out.println(getString(R.string.logNotFound));

        } catch (IOException e) {
            e.printStackTrace();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void removeListeners() {
        if (sensorEventListener != null) {
            locationManager.removeUpdates(sensorEventListener);
        }
    }

    public long getTimestamp() {    // (3)
        return timestamp;
    }
}
---


(1)(3) 開始したときのタイムスタンプとそのゲッター

    private long timestamp;    // 開始したときのタイムスタンプ        // (1)

    public long getTimestamp() {    // (3)


(2) タイムスタンプの取得

    timestamp = System.currentTimeMillis(); // タイムスタンプを取得する // (2)


◆ センサーのリスナーのクラス。

以下の GpsLogger01 プロジェクトと同じ。
位置情報 (3) / ファイル・システム (1) メモリー内のファイルに位置情報を書き込む
http://blog.goo.ne.jp/marunomarunogoo/d/20120110


◆ ログを管理するクラス

外部メディアを使うので、ローカルのときとは少し違う。
ただし、基本は一緒で、ログへの出力は PrintWriter オブジェクト、ログの取得は、
BufferedReader オブジェクトを作るのは変わらない。

外部メディア関係を扱うクラスは Environement クラス。
ここが、メモリー内のファイルとは違う部分である。

□ LogDao.java
---
package jp.marunomaruno.android.gpslogger;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;

import android.content.Context;
import android.location.Location;
import android.os.Environment;

/**
 * 位置情報ログにアクセスするクラス。
 * すべてのメソッドはstatic。
 * @author marunomaruno
 */
public class LogDao {

    public static final String SEPARATER_CHARACTER = ",";

    private LogDao() {
    }

    /**
     * ログファイルを取得する。
     * このファイルがない場合は、必要なディレクトリーも含めてファイルを生成する。
     * @param context コンテキスト
     * @return ログファイル
     * @throws IOException 
     */
    public static File getFile(Context context) throws IOException {
        String status = Environment.getExternalStorageState();        // (1)
        if (!status.equals(Environment.MEDIA_MOUNTED)) {             // (2)
            throw new IOException(context.getString(R.string.mediaDisable));
        }
        
        File file = new File(String.format(context
                .getString(R.string.logFilePathFormat), 
                Environment.getExternalStorageDirectory(),             // (3)
                context.getPackageName(),
                context.getString(R.string.logFilePrefix), 
                ((GpsLogger02Activity) context).getTimestamp(),
                context.getString(R.string.logFileExtension)));        // (4)

        File directory = file.getParentFile();

        if (!directory.exists()) {                                    // (5)
            directory.mkdirs();
        }

        return file;
    }

    /**
     * locationのデータをログファイルにCSV形式で書き出す。
     * @param context
     * @param location
     * @throws UnsupportedEncodingException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public static void write(Context context, Location location)
            throws UnsupportedEncodingException, FileNotFoundException,
            IOException {
        PrintWriter out = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream(getFile(context), true), context
                        .getString(R.string.logFileEncoding)));        // (6)

        out.printf("¥"%1$tF %1$tT¥"", location.getTime()); 
                                            // ファイルをわかりやすくするため
        out.print(SEPARATER_CHARACTER);

        out.print(location.getTime());
        out.print(SEPARATER_CHARACTER);
        out.print(location.getLatitude());
        out.print(SEPARATER_CHARACTER);
        out.print(location.getLongitude());
        out.print(SEPARATER_CHARACTER);
        out.print(location.getAccuracy());
        out.print(SEPARATER_CHARACTER);
        out.print(location.getAltitude());
        out.print(SEPARATER_CHARACTER);
        out.print(location.getSpeed());
        out.print(SEPARATER_CHARACTER);
        out.print(location.getBearing());
        out.println();
        out.close();
    }

    /**
     * ログファイルの内容を標準出力にプリントする。
     * @param context
     * @throws UnsupportedEncodingException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public static void print(Context context)
            throws UnsupportedEncodingException, FileNotFoundException,
            IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(
                new FileInputStream(getFile(context)), context
                        .getString(R.string.logFileEncoding)));        // (7)
        String line;
        while ((line = in.readLine()) != null) {
            System.out.println(line);
        }
        in.close();
    }

    /**
     * ログファイルを削除する。
     * @param context
     * @throws IOException 
     */
    public static void delete(Context context) throws IOException {
        getFile(context).delete();                                    // (8)
    }
}
---

(1)(2) 外部メディアが使用可能かどうかを判断する

外部メディア(SD カード)の状態を取得する。

    String status = Environment.getExternalStorageState();        // (1)

取得した状態が、実機にマウントされているかどうかを判断する。されていなければ、
IOException をスローする。

    if (!status.equals(Environment.MEDIA_MOUNTED)) {             // (2)
        throw new IOException(context.getString(R.string.mediaDisable));
    }


・Environment クラス

SD カードのような、外部メディア関係を扱うユーティリティのクラスとして、
Environment クラスがある。

java.lang.Object
   +     android.os.Environment


・メソッド
---
static File     getDataDirectory()
static File     getDownloadCacheDirectory()
static File     getExternalStorageDirectory()
static File     getExternalStoragePublicDirectory(String type)
static String   getExternalStorageState()
static File     getRootDirectory()
static boolean  isExternalStorageEmulated()
static boolean  isExternalStorageRemovable()
---


・getExternalStorageState() メソッドの返り値になる定数(型はすべて String)
---
MEDIA_BAD_REMOVAL       外部メディアをアンマウントする前に取り外した
MEDIA_CHECKING          外部メディアのチェックを始めた
MEDIA_MOUNTED           外部メディアが装着されている(読み取り・書き込み可)
MEDIA_MOUNTED_READ_ONLY 外部メディアが装着されている(読み取り専用)
MEDIA_NOFS              外部メディアは装着されているが、ブランクか
                        サポートしていないファイルシステムを使っている
MEDIA_REMOVED           外部メディアが装着されていない
MEDIA_SHARED            外部メディアは USB メモリーとして共有されている(使えない状態)
MEDIA_UNMOUNTABLE       外部メディアは装着されているが、マウントできない
MEDIA_UNMOUNTED         外部メディアは装着されているが、マウントされていない
---


(3)(4) ファイル・オブジェクトの取得

    File file = new File(String.format(context
            .getString(R.string.logFilePathFormat), 
            Environment.getExternalStorageDirectory(),             // (3)
            context.getPackageName(),
            context.getString(R.string.logFilePrefix), 
            ((GpsLogger02Activity) context).getTimestamp(),
            context.getString(R.string.logFileExtension)));        // (4)

ルートからのパスを作って、ファイル・オブジェクトを取得する。
ファイル名は、リソースで定義しているフォーマットにしたがって、文字列化する。
実際の値は、次のメソッドで、
    context.getString(R.string.logFilePathFormat)
取得でき、つぎの文字列を持っている。
    %1$s/data/%2$s/%3$s%4$tY%4$tm%4$td_%4$tH%4$tM%4$tS.%5$s


それぞれの値はつぎのメソッドで取得する。(右は、このサンプルの例))
---
%1$s    Environment.getExternalStorageDirectory()        /mnt/sdcard/
%2$s    context.getPackageName()
                                               jp.marunomaruno.android.gpslogger
%3$s    context.getString(R.string.logFilePrefix)        location
%4$tX    ((GpsLogger02Activity) context).getTimestamp()  タイムスタンプ
%5$s    context.getString(R.string.logFileExtension)     txt
---


(5) ディレクトリーがないときは作成する

    if (!directory.exists()) {                                    // (5)
        directory.mkdirs();
    }


(6) locationのデータをログファイルにCSV形式で書き出す。

File オブジェクトが (3)(4) によって作れれば、これをもとにして、JavaSE と同じよう
に、PrintWriter オブジェクトを作る。

    PrintWriter out = new PrintWriter(new OutputStreamWriter(
            new FileOutputStream(getFile(context), true), context
                    .getString(R.string.logFileEncoding)));        // (6)


(7) ログファイルの内容を標準出力にプリントする。

ログファイルの読み込みも、File オブジェクトがあれば、(6) と同様に、
BufferedReader オブジェクトを作る。

    BufferedReader in = new BufferedReader(new InputStreamReader(
            new FileInputStream(getFile(context)), context
                    .getString(R.string.logFileEncoding)));        // (7)

(8) ログファイルを削除する。

File オブジェクトなので、その delete() メソッドを使えばよい。

    getFile(context).delete();                                    // (8)


◆ レイアウト

以下の GpsLogger01 プロジェクトと同じ。
位置情報 (3) / ファイル・システム (1) メモリー内のファイルに位置情報を書き込む
http://blog.goo.ne.jp/marunomarunogoo/d/20120110


◆ 文字列の定数

□ res/values/string.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">GpsLogger02</string>
    <string name="logTag">TEST</string>
    <string name="no_provider">プロバイダーは見つかりませんでした。</string>
    <string name="sensor_value_format_for_real">%1$s = %2.2f</string>
    <string name="sensor_value_format_for_timestamp">%1$s = %2$tF %2$tT</string>
    <string name="latitude">緯度</string>
    <string name="longitude">経度</string>
    <string name="accuracy">精度</string>
    <string name="altitude">標高</string>
    <string name="time">時間</string>
    <string name="speed">速度</string>
    <string name="bearing">ベアリング</string>
    <string name="logFilePrefix">location</string>
    <string name="logFileExtension">txt</string>
    <string name="logFilePathFormat">
            %1$s/data/%2$s/%3$s%4$tY%4$tm%4$td_%4$tH%4$tM%4$tS.%5$s</string> 
            <!-- /mnt/sdcard / data / app_name / location yyyymmdd_hhmmss.txt --> 
            <!-- (1) -->
    <string name="logFileEncoding">UTF-8</string>
    <string name="startButton">開始</string>
    <string name="stopButton">終了</string>
    <string name="logPrintButton">ログ表示</string>
    <string name="logClearButton">ログクリア</string>
    <string name="logNotFound">ログはありません。</string>
    <string name="startMessageFormat">開始します。ファイル名 = %s</string>
    <string name="providerDisable">無線ネットワークを使用できるようにしてください。</string>
    <string name="providerEnabled">無線ネットワークを使用します。</string>
    <string name="mediaDisable">SDカードが使用できません。</string>
</resources>
---

(1) ログファイルのパスも含めたファイル名

    <string name="logFilePathFormat">
            %1$s/data/%2$s/%3$s%4$tY%4$tm%4$td_%4$tH%4$tM%4$tS.%5$s</string> 
            <!-- /mnt/sdcard / data / app_name / location yyyymmdd_hhmmss.txt --> 
            <!-- (1) -->

たとえば、2012年1月2日6時57分14秒に開始すれば、
/mnt/sdcard/data/jp.marunomaruno.android.gpslogger/location20120102_065714.txt
になる。


◆マニフェスト

□AndroidManifest.xml
---
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.marunomaruno.android.gpslogger"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- (1) -->

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".GpsLogger02Activity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
---

(1) SD カードへの書き込み許可

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- (1) -->



[Android] 位置情報 (3) / ファイル・システム (1) メモリー内のファイルに位置情報を書き込む

2012年01月10日 | Android
[Android] 位置情報 (3) / ファイル・システム (1) メモリー内のファイルに位置情報を書き込む
================================================================================

Android で、永続化をはかる方法としては、以下の方法がある。
・SharedPreferences
・ファイル・システム
・SQLite データベース
・ContentProvider

今回は、ファイル・システムを利用する。
ファイルシステムも、メメリー内とメモリーの外(SD カードなど)にファイルを構築す
る方法で違いがある。

このサンプルは、メモリー内のファイルへの入出力を行う。


■ メモリー内のファイルに位置情報を書き込む

このアプリケーションは、つぎつぎと変化する位置情報を、メモリー内のファイル(ロー
カル・ファイル)に書き込む。また、利用者の指定によって、ファイルの内容を表示したり、
削除したりする。

また、位置情報は、コンソールに表示するように、画面にスクロールして表示される。

位置情報の取得は、「開始/終了」ボタンのクリックによる。このボタンはトグルボタン
になっていて、そのときの状態を保存する必要がある。

単なるファイルの扱いだけになっていないので、コードはある程度複雑になっているが、
ファイルの扱いの部分は、LogDao クラスのコードを参照すればよい。
トグルボタンを扱う部分と、コンソール表示コは、つぎの既出のドキュメントを参照され
たい。
    位置情報 (2) トグルボタンによる位置情報の取得の開始・終了
    http://blog.goo.ne.jp/marunomarunogoo/d/20120109

    標準出力 - あて先に画面を追加する
    http://blog.goo.ne.jp/marunomarunogoo/d/20111222


コード
---
GpsLogger01Activity.java  アクティビティ
GpsLocationListener.java  ロケーションのリスナー
TextViewPrintStream.java  System.out オブジェクトの置き換え(SyystemOut01 と同じ)
LogDao.java               ログの入出力を行うユーティリティのクラス
---


◆ アクティビティのクラス

採ったログを表示するための「ログ表示」ボタン、ログファイルをクリアする「ログクリ
ア」ボタンが追加になっているので、これに対するハンドラー・メソッド
    onClickLogPrintButton()
    onClickLogClearButton()
を追加している。メソッドは、単に LogDao のメソッドを呼び出しているだけである。


□ GpsLogger01Activity.java
---
package jp.marunomaruno.android.sample;

import java.io.FileNotFoundException;
import java.io.IOException;

import android.app.Activity;
import android.location.Criteria;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

/**
 * @author marunomaruno
 * @version 1.0, 2011-12-14
 * @since 1.0
 */
public class GpsLogger01Activity extends Activity {

    private boolean isRunning = false;    // 現在起動中かどうか
    private static final String IS_RUNNING = "isRunning";

    private LocationManager locationManager;
    private LocationListener sensorEventListener; 
                // onPause()が呼ばれたときに、すべてのリスナーを解除するため
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d(getString(R.string.logTag), String.format("onCreate() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // 標準出力を置き換え(標準出力(LogCat)+テキストビュー)
        TextView sysout = (TextView) findViewById(R.id.sysout);
        System.setOut(new TextViewPrintStream(System.out, sysout));
        System.setErr(new TextViewPrintStream(System.err, sysout));

        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
    }

    @Override
    protected void onResume() {
        Log.d(getString(R.string.logTag), String.format("onResume() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onResume();

        Button startStopButton = (Button) findViewById(R.id.startStopButton);

        Log.d(getString(R.string.logTag), "起動中 = " + isRunning);
        
        if (isRunning) {    // 起動中のとき、終了ボタンを表示する
            startStopButton.setText(R.string.stopButton);

        } else {
            startStopButton.setText(R.string.startButton);
        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        Log.d(getString(R.string.logTag), String.format("onRestoreInstanceState() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onRestoreInstanceState(savedInstanceState);
        isRunning = savedInstanceState.getBoolean(IS_RUNNING);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        Log.d(getString(R.string.logTag), String.format("onSaveInstanceState() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onSaveInstanceState(outState);
        outState.putBoolean(IS_RUNNING, isRunning);
    }

    /**
     * 開始/終了ボタン押下時
     * @param view
     */
    public void onClickStartStopButton(View view) {
        Log.d(getString(R.string.logTag), String.format("onClickStartButton() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        Button startStopButton = (Button) view;
        
        if (isRunning) {    // 起動中のとき、とめる
            // すべてのリスナーを解除する
            removeListeners();                     

            // 開始ボタンにする
            startStopButton.setText(R.string.startButton);

            System.out.println(getString(R.string.stopButton));
            System.out.println(String.format(
                        getString(R.string.sensor_value_format_for_timestamp),
                        getString(R.string.stopButton), System
                                .currentTimeMillis()));

        } else {
            // 起動していないとき、
            // GPSロケーションを取得して登録する
            setProviderAndListener();
            
            // 停止ボタンにする
            startStopButton.setText(R.string.stopButton);
            System.out.printf(getString(R.string.startMessageFormat), LogDao
                    .getFile(this));    // (1)
            System.out.println();
        }
        isRunning = !isRunning;    // 起動中状態を逆にする
        Log.d(getString(R.string.logTag), String.format(
                "起動中 = %b, 開始時刻  = %tT, ファイル名 = %s", isRunning, System
                        .currentTimeMillis(), LogDao.getFile(this)));
    }

    private void setProviderAndListener() {
        // プロバイダーを取得する条件を作成する
        Criteria fineAndLowPower = new Criteria();
        fineAndLowPower.setAccuracy(Criteria.ACCURACY_FINE);
        fineAndLowPower.setPowerRequirement(Criteria.POWER_LOW);
        
        // プロバイダーを取得する
        String provider = locationManager.getBestProvider(fineAndLowPower, true);
        Log.d(getString(R.string.logTag), "provider = "
                + provider);
        TextView providerNameTextView = (TextView) findViewById(R.id.provider_name);
        providerNameTextView.setText(provider);
        
        // プロバイダーがみつからなければ、NETWORK_PROVIDER を設定しておく
        if (provider == null) {
            provider = LocationManager.NETWORK_PROVIDER;
            providerNameTextView.setText(R.string.no_provider);
        }
        
        // 位置情報のリスナーを取得して登録する
        sensorEventListener = GpsLocationListener.getInstance(this);
        locationManager.requestLocationUpdates(
                provider, 
                0, 
                0, 
                sensorEventListener);
    }

    /**
     * ログをクリアする。
     * @param view
     */
    public void onClickLogClearButton(View view) {
        LogDao.delete(this);                                   // (2)
    }

    /**
     * ログを表示する
     * @param view
     */
    public void onClickLogPrintButton(View view) { 
        try {
            LogDao.print(this);                                // (3)
        } catch (FileNotFoundException e) {
            System.out.println(getString(R.string.logNotFound));

        } catch (IOException e) {
            e.printStackTrace();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void removeListeners() {
        if (sensorEventListener != null) {
            locationManager.removeUpdates(sensorEventListener);
        }
    }
}
---

(1) ログのファイル名を取得する

    System.out.printf(getString(R.string.startMessageFormat), LogDao
            .getFile(this));    // (1)

LogDao は、ユーティリティのクラスとして、すべてのメソッドは static になっている。
基本的には、アクティビティのオブジェクトを渡して、処理する。


(2) ログをクリアする

    LogDao.delete(this);                                   // (2)


(3) ログを表示する

    LogDao.print(this);                                    // (3)


◆ センサーのリスナーのクラス。

Gps01 プロジェクトからみると、変更している箇所は、位置情報を標準出力に表示すると
ころを、ログにも書き込む 1 箇所だけ。

□ GpsLocationListener.java
---
package jp.marunomaruno.android.sample;

import java.io.FileNotFoundException;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationProvider;
import android.os.Bundle;
import android.util.Log;

/**
 * GPSによるロケーションのリスナー
 * @author maruno
 * @version 1.0, 2011-12-14
 * @since 1.0
 */
public class GpsLocationListener implements LocationListener {

    protected Context context;
    private static LocationListener instance;
    
    private GpsLocationListener(Context context) {
        this.context = context;
    }
    
    /**
     * このリスナーの唯一のインスタンスを取得する。
     * @param context
     * @return このインスタンス
     */
    public static LocationListener getInstance(Context context) {
        if (instance == null) {
            instance = new GpsLocationListener(context);
        }
        return instance;
    }

    @Override
    public void onLocationChanged(Location location) {
        Log.d(context.getString(R.string.logTag),
                String.format("onLocationChanged() " +  context.getString(
                        R.string.sensor_value_format_for_timestamp), 
                        context.getString(R.string.time), 
                        System.currentTimeMillis()));
        
        System.out.println("---");

        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_timestamp), 
                context.getString(R.string.time), 
                location.getTime()));                                    

        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_real), 
                context.getString(R.string.latitude), 
                location.getLatitude()));                    

        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_real), 
                context.getString(R.string.longitude), 
                location.getLongitude()));                    
        
        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_real), 
                context.getString(R.string.accuracy), 
                location.getAccuracy()));                    

        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_real), 
                context.getString(R.string.altitude), 
                location.getAltitude()));                    
        
        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_real), 
                context.getString(R.string.speed), 
                location.getSpeed()));                
        
        System.out.println(String.format(context.getString(
                R.string.sensor_value_format_for_real), 
                context.getString(R.string.bearing), 
                location.getBearing()));                        

        try {
            LogDao.write(context, location);                    // (1)

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onProviderDisabled(String provider) {
        System.out.println(context.getString(R.string.providerDisable));
    }

    @Override
    public void onProviderEnabled(String provider) {
        System.out.println(context.getString(R.string.providerEnabled));
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        Log.d(context.getString(R.string.logTag),
                String.format("onStatusChanged() " +  context.getString(
                        R.string.sensor_value_format_for_timestamp), 
                        context.getString(R.string.time), 
                        System.currentTimeMillis()));

        switch (status) {
        case LocationProvider.AVAILABLE:
            Log.d(context.getString(R.string.logTag),
                    "onStatusChanged() - AVAILABLE");
            break;
        case LocationProvider.OUT_OF_SERVICE:
            Log.d(context.getString(R.string.logTag),
                    "onStatusChanged() - OUT_OF_SERVICE");
            break;
        case LocationProvider.TEMPORARILY_UNAVAILABLE:
            Log.d(context.getString(R.string.logTag),
                    "onStatusChanged() - TEMPORARILY_UNAVAILABLE");
            break;
        default:
            Log.d(context.getString(R.string.logTag),
                    "onStatusChanged() - Other");
            break;
        }
    }
}
---

(1) 位置情報をログに書き込む

    LogDao.write(context, location);                    // (1)


◆ ログを管理するクラス

位置情報ログにアクセスするクラス。すべてのメソッドは static となっているユーティ
リティのクラス。

つぎのメソッドがある。
---
static void delete(Context context) 
          ログファイルを削除する。 

static File getFile(Context context) 
          ログファイルを取得する。 

static void print(Context context) 
          ログファイルの内容を標準出力にプリントする。 

static void write(Context context, Location location) 
          location のデータをログファイルに CSV 形式で書き出す。 
---

□ LogDao.java
---
package jp.marunomaruno.android.sample;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;

import android.content.Context;
import android.location.Location;

/**
 * 位置情報ログにアクセスするクラス。
 * すべてのメソッドはstatic。
 * @author marunomaruno
 * @version 1.0, 2011-12-14
 * @since 1.0
 */
public class LogDao {

    private LogDao() {
    }

    /**
     * ログファイルを取得する。
     * @param context コンテキスト
     * @return ログファイル
     */
    public static File getFile(Context context) {                    // (1)
        File file = new File(context.getString(R.string.logFilePrefix) + "."
                + context.getString(R.string.logFileExtension));    // (2)
        return file;
    }

    /**
     * locationのデータをログファイルにCSV形式で書き出す。
     * @param context
     * @param location
     * @throws UnsupportedEncodingException
     * @throws FileNotFoundException
     */
    public static void write(Context context, Location location)    // (3)
            throws UnsupportedEncodingException, FileNotFoundException {
        PrintWriter out = new PrintWriter(new OutputStreamWriter(context
                .openFileOutput(getFile(context).getName(), Context.MODE_APPEND
                        | Context.MODE_WORLD_READABLE), context.getResources()
                .getString(R.string.logFileEncoding)));                // (4)

        out.printf("¥"%1$tF %1$tT¥"", location.getTime()); 
                                // ファイルをわかりやすくするため    // (5)
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getTime());
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getLatitude());
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getLongitude());
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getAccuracy());
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getAltitude());
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getSpeed());
        out.print(context.getString(R.string.csvSeparateCharacter));
        out.print(location.getBearing());
        out.println();
        out.close();
    }

    /**
     * ログファイルの内容を標準出力にプリントする。
     * @param context
     * @throws UnsupportedEncodingException
     * @throws FileNotFoundException
     * @throws IOException
     */
    public static void print(Context context)                        // (6)
            throws UnsupportedEncodingException, FileNotFoundException,
            IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(context
                .openFileInput(getFile(context).getName()), context
                .getString(R.string.logFileEncoding)));                // (7)
        String line;
        while ((line = in.readLine()) != null) {
            System.out.println(line);
        }
        in.close();
    }

    /**
     * ログファイルを削除する。
     * @param context
     */
    public static void delete(Context context) {                    // (8)
        context.deleteFile(getFile(context).getName());                // (9)
    }
}
---

(1)(2) ファイルを取得する

    public static File getFile(Context context) {                    // (1)

引数のコンテキストから、リソースで指定してある名前を基に、ファイル・オブジェクト
を取得する。JavaSE と同じ方法で取得できる。

    File file = new File(context.getString(R.string.logFilePrefix) + "."
            + context.getString(R.string.logFileExtension));    // (2)


(3)(4)(5) location のデータをログファイルに CSV 形式で書き出す

    public static void write(Context context, Location location)    // (3)
            throws UnsupportedEncodingException, FileNotFoundException {

CSV ファイル(テキスト・ファイル)なので、扱いやすい PrintWriter オブジェクトとす
る。

        PrintWriter out = new PrintWriter(new OutputStreamWriter(context
                .openFileOutput(getFile(context).getName(), Context.MODE_APPEND
                        | Context.MODE_WORLD_READABLE), context.getResources()
                .getString(R.string.logFileEncoding)));                // (4)


JavaSE と同じ部分として、つぎのように、指定された文字セットを使う 
OutputStreamWriter を作成する部分がある。

    PrintWriter out = new PrintWriter(new OutputStreamWriter(OutputStream out, 
										String charsetName));

上記の out オブジェクトは、Context クラスの openFileOutput() メソッドによる。

    context.openFileOutput(getFile(context).getName(), Context.MODE_APPEND
                        | Context.MODE_WORLD_READABLE), 


Context クラスの FileOutputStream オブジェクトを開くメソッド
---
abstract FileOutputStream     openFileOutput(String name, int mode)
---

ファイル名 name は、この LogDao クラスの getFile() メソッドを使えばよい。

モード mode は、ファイル操作のモードで、次のものがある。
---
MODE_PRIVATE          他のアプリからアクセス不可(デフォルト)  0x00000000
MODE_WORLD_READABLE   他のアプリから読み込み可                0x00000001
MODE_WORLD_WRITEABLE  他のアプリから書き込み可                0x00000002
MODE_APPEND           既存ファイルに追加可                    0x00008000
---

これらのモードは、ビットに対応しているので、論理和を使って組み合わせが可能。


文字セット charsetName は、リソースファイルに指定してあるものを使う。
    context.getResources().getString(R.string.logFileEncoding)


PrintWriter オブジェクトができれば、あとは、通常の out オブジェクトを書くのと一
緒。

    out.printf("¥"%1$tF %1$tT¥"", location.getTime()); // ファイルをわかりやすく
するため    // (5)


(6)(7) ログファイルの内容を標準出力にプリントする。

    public static void print(Context context)                        // (6)


BufferedReader オブジェクトを作って扱う。作り方は、PrintWriter オブジェクトを作
るのと同様。ローカルのファイルを入力モードで開くのは、openFileInput() メソッドを
使う。

    BufferedReader in = new BufferedReader(new InputStreamReader(context
            .openFileInput(getFile(context).getName()), context
            .getString(R.string.logFileEncoding)));                // (7)


Context クラスの FileInputStream オブジェクトを開くメソッド
---
abstract FileInputStream     openFileInput(String name)
---


(8)(9) ログファイルを削除する

    public static void delete(Context context) {                    // (8)

ローカルのファイルを削除するのは、deleteFile() メソッドを使う。

        context.deleteFile(getFile(context).getName());                // (9)

---
abstract boolean     deleteFile(String name)
---


・Context クラスのファイル関係と思われるメソッド
---
abstract boolean   deleteFile(String name)
abstract String[]  fileList()
abstract File      getCacheDir()
abstract File      getDir(String name, int mode)
abstract File      getExternalCacheDir()
abstract File      getExternalFilesDir(String type)
abstract File      getFileStreamPath(String name)
abstract File      getFilesDir()
abstract String    getPackageCodePath()
abstract String    getPackageResourcePath()
abstract FileInputStream   openFileInput(String name)
abstract FileOutputStream  openFileOutput(String name, int mode)
---

詳細は API 参照。


◆ レイアウト

以下の Gps02 プロジェクトと同じ。
位置情報 (2) トグルボタンによる位置情報の取得の開始・終了
http://blog.goo.ne.jp/marunomarunogoo/d/20120109


◆ 文字列の定数

□ res/values/string.xml

センサーの名前と合わせて、そのセンサーで取得する値の単位も定義した。

□ res/values/string.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">GpsLogger01</string>
    <string name="logTag">TEST</string>
    <string name="no_provider">プロバイダーは見つかりませんでした。</string>
    <string name="sensor_value_format_for_real">%1$s = %2.2f</string>
    <string name="sensor_value_format_for_timestamp">%1$s = %2$tF %2$tT</string>
    <string name="latitude">緯度</string>
    <string name="longitude">経度</string>
    <string name="accuracy">精度</string>
    <string name="altitude">標高</string>
    <string name="time">時間</string>
    <string name="speed">速度</string>
    <string name="bearing">ベアリング</string>
    <string name="logFilePrefix">location</string> <!-- (1) ファイル名 -->
    <string name="logFileExtension">txt</string>   <!-- (2) 拡張子 -->
    <string name="logFileEncoding">UTF-8</string>  <!-- (3) 文字コード -->
    <string name="csvSeparateCharacter">,</string> <!-- (4) CSVファイルの分離符 -->
    <string name="startButton">開始</string>
    <string name="stopButton">終了</string>
    <string name="logPrintButton">ログ表示</string>
    <string name="sendMailButton">メール送信</string>
    <string name="logClearButton">ログクリア</string>
    <string name="logNotFound">ログはありません。</string>
    <string name="startMessageFormat">開始します。ファイル名 = %s</string>
    <string name="providerDisable">無線ネットワークを使用できるようにしてください。
	</string>
    <string name="providerEnabled">無線ネットワークを使用します。</string>
</resources>
---


◆マニフェスト

以下の Gps01 プロジェクトと同じ。
位置情報 (1) 無線ネットワークを使った位置情報
http://blog.goo.ne.jp/marunomarunogoo/d/20111218



[Android] 位置情報(2) トグルボタンによる位置情報の取得の開始・終了

2012年01月09日 | Android
位置情報(2) トグルボタンによる位置情報の取得の開始・終了
================================================================================

■ 位置情報の取得の開始・終了をトグルボタンで制御する

位置情報の取得は、「開始/終了」ボタンのクリックによる。このボタンはトグルボタン
になっていて、そのときの状態を保存する必要がある。

また、位置情報は、コンソールに表示するように、画面にスクロールして表示される。

トグルボタンは、実行状態をインスタンス変数で保持する。ただし、画面が切り替わった
ときなどは、Activity のインスタンスは新しく生成されるため、Activity クラスの
    onSaveInstanceState()
    onRestoreInstanceState()
メソッドをオーバーライドして、保存して、復帰する必要がある。

コンソール表示のイメージは、つぎの既出のドキュメントを参照されたい。
    標準出力 - あて先に画面を追加する
    http://blog.goo.ne.jp/marunomarunogoo/d/20111222

コード
---
Gps02Activity.java        アクティビティ
GpsLocationListener.java  ロケーションのリスナー(Gps01 と同じ)
TextViewPrintStream.java  System.out オブジェクトの置き換え(ystemOut01 と同じ)
---

Gps01 と同じ、というのは、以下のドキュメントの Gps01 プロジェクトのソースと同じ
ソースである。
    位置情報 (1) 無線ネットワークを使った位置情報
    http://blog.goo.ne.jp/marunomarunogoo/d/20111218


◆ アクティビティのクラス

□ Gps02Activity.java
---
package jp.marunomaruno.android.sample;

import android.app.Activity;
import android.location.Criteria;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

/**
 * 位置情報を表示するアクティビティ。
 * トグルボタンで、開始・終了する。
 * @author marunomaruno
 * @version 1.0, 2011-12-14
 * @since 1.0
 */
public class Gps02Activity extends Activity {

    private boolean isRunning = false;    // 現在起動中かどうか        // (1)
    private static final String IS_RUNNING = "isRunning";      // (2)

    private LocationManager locationManager;
    private LocationListener sensorEventListener; 
                    // onPause()が呼ばれたときに、すべてのリスナーを解除するため
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d(getString(R.string.logTag), String.format("onCreate() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // 標準出力を置き換え(標準出力(LogCat)+テキストビュー)
        TextView sysout = (TextView) findViewById(R.id.sysout);    // (3)
        System.setOut(new TextViewPrintStream(System.out, sysout));    // (4)
        System.setErr(new TextViewPrintStream(System.err, sysout));    // (5)

        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
    }

    @Override
    protected void onResume() {
        Log.d(getString(R.string.logTag), String.format("onResume() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onResume();

        Button startStopButton = (Button) findViewById(R.id.startStopButton);

        Log.d(getString(R.string.logTag), "起動中 = " + isRunning);
        
        if (isRunning) {    // 起動中のとき、終了ボタンを表示する      // (6)
            startStopButton.setText(R.string.stopButton);    // (7)

        } else {
            startStopButton.setText(R.string.startButton);    // (8)
        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {    // (9)
        Log.d(getString(R.string.logTag), String.format("onRestoreInstanceState() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onRestoreInstanceState(savedInstanceState);
        isRunning = savedInstanceState.getBoolean(IS_RUNNING);        // (10)
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {            // (11)
        Log.d(getString(R.string.logTag), String.format("onSaveInstanceState() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        super.onSaveInstanceState(outState);
        outState.putBoolean(IS_RUNNING, isRunning);                    // (12)
    }

    /**
     * 開始/終了ボタン押下時
     * @param view
     */
    public void onClickStartStopButton(View view) {                    // (13)
        Log.d(getString(R.string.logTag), String.format("onClickStartButton() "
                + getString(R.string.sensor_value_format_for_timestamp),
                getString(R.string.time), System.currentTimeMillis()));

        Button startStopButton = (Button) view;
        
        if (isRunning) {    // 起動中のとき、とめる    // (14)
            // すべてのリスナーを解除する
            removeListeners();

            // 開始ボタンにする
            startStopButton.setText(R.string.startButton);
            System.out.printf(getString(R.string.stopMessageFormat), 
                    System.currentTimeMillis());
            System.out.println();

        } else {
            // 起動していないとき、
            // GPSロケーションを取得して登録する
            setProviderAndListener();
            
            // 停止ボタンにする
            startStopButton.setText(R.string.stopButton);
            System.out.printf(getString(R.string.startMessageFormat), 
                    System.currentTimeMillis());
            System.out.println();
        }
        isRunning = !isRunning;    // 起動中状態を逆にする            // (15)
        Log.d(getString(R.string.logTag), String.format(
                "起動中 = %b, 開始時刻  = %tT", isRunning, System
                        .currentTimeMillis()));
    }

    private void setProviderAndListener() {
        // プロバイダーを取得する条件を作成する
        Criteria fineAndLowPower = new Criteria();
        fineAndLowPower.setAccuracy(Criteria.ACCURACY_FINE);
        fineAndLowPower.setPowerRequirement(Criteria.POWER_LOW);
        
        // プロバイダーを取得する
        String provider = locationManager.getBestProvider(fineAndLowPower, true);
        Log.d(getString(R.string.logTag), "provider = "
                + provider);
        TextView providerNameTextView = (TextView) findViewById(R.id.provider_name);
        providerNameTextView.setText(provider);
        
        // プロバイダーがみつからなければ、NETWORK_PROVIDER を設定しておく
        if (provider == null) {
            provider = LocationManager.NETWORK_PROVIDER;
            providerNameTextView.setText(R.string.no_provider);
        }
        
        // 位置情報のリスナーを取得して登録する
        sensorEventListener = GpsLocationListener.getInstance(this);
        locationManager.requestLocationUpdates(
                provider, 
                0, 
                0, 
                sensorEventListener);
    }

    private void removeListeners() {
        if (sensorEventListener != null) {
            locationManager.removeUpdates(sensorEventListener);
        }
    }
}
---

(1)(2) 現在起動中かどうかをあらわすインスタンス変数

    private boolean isRunning = false;    // 現在起動中かどうか        // (1)

トグルボタンを設定するために、現在の状態(起動中/停止中)をあらわすフラグをインス
タンス変数として保持する。
これは、(9)の onSaveInstanceState() メソッドで保存して、 (8)の 
onRestoreInstanceState() メソッドで復帰することで、画面が隠れてまた現れたときに、
別のアクティビティのインスタンスが生成されても、データが保持されるようにしておく
ためである。
この、保存するときのキーは、文字列なので、つぎのような定数を用意しておく。

    private static final String IS_RUNNING = "isRunning";      // (2)


(3)-(5) 標準出力を置き換え(標準出力(LogCat)+テキストビュー)

標準出力と標準エラー出力を置き換える。

    TextView sysout = (TextView) findViewById(R.id.sysout);    // (3)
    System.setOut(new TextViewPrintStream(System.out, sysout));    // (4)
    System.setErr(new TextViewPrintStream(System.err, sysout));    // (5)

コンソール表示のイメージは、つぎの既出のドキュメントを参照されたい。
    標準出力 - あて先に画面を追加する
    http://blog.goo.ne.jp/marunomarunogoo/d/20111222


(6)-(8) 起動中と停止中で、トグルボタンの表示を切り替える

    if (isRunning) {    // 起動中のとき、終了ボタンを表示する      // (6)
        startStopButton.setText(R.string.stopButton);    // (7)

    } else {
        startStopButton.setText(R.string.startButton);    // (8)
    }


(9) onRestoreInstanceState() メソッドのオーバーライド

onSaveInstanceState() メソッドで保存されたインスタンスの状態を、アクティビティが
再初期化される直前に呼び出される

    protected void onRestoreInstanceState(Bundle savedInstanceState) {    // (9)


(10) 実行状態を復帰する

isRunning は boolean 型なので、Bundle クラスの getBoolean() メソッドを使う。

    isRunning = savedInstanceState.getBoolean(IS_RUNNING);        // (10)

保存されている型によって、さまざまな取得メソッドがある。

・基本的な型を取得するメソッド
---
xxx    getXxx(String key)
xxx    getXxx(String key, xxx defaultValue)
xxx[]  getXxxArray(String key)
---
    xxx: boolean, byte, char, double, float, int, long, short, CharSequence, 
         String, Parcelable

なお、CharSequence, int, String は、上記に加えてつぎのArrayListを保存できるメソ
ッドがある。
---
ArrayList<xxx>  getXxxArrayList(String key)
---
Parcelable 型については、ジェネリクスとして、型が考えられている。詳しくは、API 
を参照。

上記以外に、つぎのメソッドがある。
---
Object          get(String key)
Bundle          getBundle(String key)
Serializable    getSerializable(String key)
<T extends Parcelable> SparseArray<T>     
                getSparseParcelableArray(String key)
---


(11) onSaveInstanceState() メソッドのオーバーライド

インスタンスの状態をアクティビティに保存するときに呼び出される。

    protected void onSaveInstanceState(Bundle outState) {            // (11)


(12) 実行状態を保存する

        outState.putBoolean(IS_RUNNING, isRunning);                    // (12)

取り出すためのキー(文字列)と、保存したい値を指定して、保存する。
保存したものは、対応する getXxx() メソッドによって取得する。

・基本的な型を保存するメソッド
---
void  putXxx(String key, xxx value)
void  putXxxArray(String key, xxx[] value)
---
    xxx: boolean, byte, char, int, double, float, long, short, CharSequence, 
         String, Parcelable

なお、CharSequence, int, String は、上記に加えてつぎのArrayListを保存できるメソ
ッドがある。
---
    void  putXxxArrayList(String key, ArrayList<xxx> value)
---
Parcelable 型については、ジェネリクスとして、型が考えられている。詳しくは、API 
を参照。

上記以外に、つぎのメソッドがある。
---
void  putBundle(String key, Bundle value)
void  putSerializable(String key, Serializable value)
void  putSparseParcelableArray(String key, SparseArray<? extends Parcelable> value)
---

ただ、不思議なのは、Object 型を取得するための
    Object     get(String key)
メソッドがあるのに、これに対する
    void     put(String key, Object value)
がない。なぜ?
一般的なオブジェクトを保存したい場合は、シリアライズして保存することになるのか?
または、いちいち Parcelable インターフェースを実装したクラスを作るのか?
Serializable の実装の方が簡単かな。


(13) 開始/終了ボタン押下時
    public void onClickStartStopButton(View view) {                    // (13)


(14) 実行状態によって、動きを変える

    if (isRunning) {    // 起動中のとき、とめる    // (14)


(15) 起動中状態を逆にする

    isRunning = !isRunning;    // 起動中状態を逆にする            // (15)

トグルボタンで、boolean 型なので、否定すればよい。


◆ 既出のソースの再掲

つぎのソースは、以下のドキュメントと同じである。
    GpsLocationListener.java
    TextViewPrintStream.java

位置情報 (1) 無線ネットワークを使った位置情報
http://blog.goo.ne.jp/marunomarunogoo/d/20111218


◆ レイアウト

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/startStopButton"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:onClick="onClickStartStopButton"
            android:text="@string/startButton" />

    </LinearLayout>

    <!-- プロバイダー名 -->
    <TextView
        android:id="@+id/provider_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
        
    <!-- センサー値 (1) -->
    <ScrollView
        android:id="@+id/sysoutScrollView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >

        <TextView
            android:id="@+id/sysout"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </ScrollView>

</LinearLayout>
---

(1) センサー値を表示する部分は、コンソール表示のイメージ部分なので、つぎの既出の
ドキュメントを参照されたい。
    標準出力 - あて先に画面を追加する
    http://blog.goo.ne.jp/marunomarunogoo/d/20111222


◆ 文字列

□ res/values/strings.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Gps02</string>
    <string name="logTag">TEST</string>
    <string name="no_provider">プロバイダーは見つかりませんでした。</string>
    <string name="sensor_value_format_for_real">%1$s = %2.2f</string>
    <string name="sensor_value_format_for_timestamp">%1$s = %2$tF %2$tT</string>
    <string name="latitude">緯度</string>
    <string name="longitude">経度</string>
    <string name="accuracy">精度</string>
    <string name="altitude">標高</string>
    <string name="time">時間</string>
    <string name="speed">速度</string>
    <string name="bearing">ベアリング</string>
    <string name="startButton">開始</string>
    <string name="stopButton">終了</string>
    <string name="startMessageFormat">開始します。時間 = %1$tF %1$tT</string>
    <string name="stopMessageFormat">終了します。時間 = %1$tF %1$tT</string>
    <string name="providerDisable">無線ネットワークを使用できるようにしてください。</string>
    <string name="providerEnabled">無線ネットワークを使用します。</string>
</resources>
---


◆マニフェスト

以下の Gps01 プロジェクトと同じ。
位置情報 (1) 無線ネットワークを使った位置情報
http://blog.goo.ne.jp/marunomarunogoo/d/20111218



[Java] 対称群と置換のユーティリティ

2012年01月06日 | Java
[Java] 対称群と置換のユーティリティ
================================================================================

15 ゲームの不可能性に関して、その 15 ゲームをあらわす置換の符号が 1 の場合は、
ゲームが解けるが、-1 の場合は解けない。

これについて、その符号を取り出してみるユーティリティを作った。
これだけではなく、関数としては、次のものがある。
---
static int[]   getPermutation(int size) 
               指定された要素数の置換を取得する。 

static boolean isPermutation(int[] permutation) 
               指定された配列が置換になっているかどうかを確認する。 

static int     sgn(int[] permutation) 
               指定された置換の符号を取得する。 
---

■ 参考 URL

15ゲームの不可能性の評価
http://www004.upp.so-net.ne.jp/s_honma/game15.htm

「対称群と15ゲーム」スライド
http://ocw.tsukuba.ac.jp/25a0-v-1-65705b66985e/65705b66727952258b1b7fa9i/300c5bfe79f07fa430681530fc30e0300d30b930e930a4-pdf30d530a130a430eb


■ ソース

コードの内容については、中のコメントを参考。とくに、ここでは補足しない。
また、今後(気が向けば)関数を追加していく。

□ SymmetricGroupUtil.java
---
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 対称群関係のユーティリティ。
 * 
 * @author marunomaruno
 * @version 1.0, 2012-01-06
 */
public class SymmetricGroupUtil {

    private SymmetricGroupUtil() {
        super();
    }

    /**
     * 指定された置換の符号を取得する。
     * 
     * @param permutation
     *            置換を意味する配列
     * @return 置換の符号
     */
    public static int sgn(int[] permutation) {
        System.out.printf("a=%s; [", Arrays.toString(permutation));

        // 置換群かどうかを確認する
        if (!isPermutation(permutation)) {
            throw new IllegalArgumentException(
                            "指定された配列は、置換群になっていません。");
        }

        // ゼロ相対の添字と合わせるので、値を1引いたものにする
        int[] sigma = new int[permutation.length];
        for (int i = 0; i < permutation.length; i++) {
            sigma[i] = permutation[i] - 1;
        }

        // 転倒数を数える
        int count = 0;
        for (int i = 0; i < sigma.length; i++) {
            // すでに循環置換として抽出したものは除く
            if (sigma[i] < 0) {
                continue;
            }

            // 巡回置換の(長さ-1)を加える
            count += (length(sigma, i) - 1);
        }

        System.out.printf("]; count=%d%n", count);

        return (count % 2 == 0) ? 1 : -1; // 本来は、(int) Math.pow(-1, count);

    }

    /**
     * 指定された置換の、指定された添字からの巡回置換の長さを取得する。 
     * なお、引数の置換は、中身が変更される可能性がある。
     * 
     * @param permutation
     *            置換
     * @param from
     *            添字
     * @return 指定された置換の、指定された添字からの巡回置換の長さ
     */
    private static int length(int[] permutation, int from) {
        int length = 0;
        System.out.print(String.format("[%d", permutation[from] + 1));
        int i = permutation[from];
        while (permutation[from] >= 0 && i != from) {
            System.out.print(String.format(", %d", permutation[i] + 1));
            length++;

            // つぎの循環置換の添字を取得し、循環置換に採用した要素を消す
            int k = i;
            i = permutation[i];
            permutation[k] = -1; // 要素を消す
        }

        length++;
        System.out.printf("](%d), ", length);
        return length;
    }

    /**
     * 指定された配列が置換になっているかどうかを確認する。
     *  この置換は、次の条件を満たしている配列。 
     *  ・要素数 n が 1 以上 
     *  ・値は 1 ~ n までのそれぞれがひとつずつ入っている
     * 
     * @param permutation
     *            置換の配列
     * @return 置換になっていれば true
     */
    public static boolean isPermutation(int[] permutation) {
        // ひとつ以上の要素を持つ
        if (permutation.length < 1) {
            return false;
        }

        // 置換をコピーして、ソートする
        int[] sigma = Arrays.copyOf(permutation, permutation.length);
        Arrays.sort(sigma);

        // それぞれの添字がその要素値と違えば、置換にはなっていない
        for (int i = 0; i < sigma.length; i++) {
            if (sigma[i] != i + 1) {
                return false;
            }
        }

        return true;
    }

    /**
     * 指定された要素数の置換を取得する。
     * 
     * @param size
     *            要素数
     * @return 置換
     */
    public static int[] getPermutation(int size) {
        // ひとつ以上の要素を持つ
        if (size < 1) {
            throw new IllegalArgumentException(String.format(
                    "置換の要素数は1以上です。(%d)%n", size));
        }

        List<Integer> sigma = new ArrayList<Integer>(size);
        for (int i = 0; i < size; i++) {
            sigma.add(i + 1);
        }

        Collections.shuffle(sigma); // シャッフルする

        int[] permutation = new int[size];
        for (int i = 0; i < size; i++) {
            permutation[i] = sigma.get(i);
        }

        assert isPermutation(permutation); // 事後条件
        return permutation;

    }
}
---


■ JUnit のテストケース

□ SymmetricGroupUtilTest.java
---
import java.util.Arrays;

import junit.framework.TestCase;

public class SymmetricGroupUtilTest extends TestCase {

    public void testSgn() {
        assertEquals(1, SymmetricGroupUtil.sgn(new int[] { 1, }));
        assertEquals(1, SymmetricGroupUtil.sgn(new int[] { 1, 2, 3, }));
        assertEquals(1, SymmetricGroupUtil.sgn(new int[] { 2, 3, 1, }));
        assertEquals(1, SymmetricGroupUtil.sgn(new int[] { 3, 1, 2, }));
        assertEquals(-1, SymmetricGroupUtil.sgn(new int[] { 1, 3, 2, }));
        assertEquals(-1, SymmetricGroupUtil.sgn(new int[] { 2, 1, 3, }));
        assertEquals(-1, SymmetricGroupUtil.sgn(new int[] { 3, 2, 1, }));
        assertEquals(1, SymmetricGroupUtil.sgn(new int[] { 3, 7, 1, 2, 5, 8, 4,
                6, }));
        assertEquals(1, SymmetricGroupUtil.sgn(new int[] { 1, 2, 3, 4, 5, 6, 7,
                8, 9, 10, 11, 12, 13, 14, 15, }));
        assertEquals(-1, SymmetricGroupUtil.sgn(new int[] { 1, 2, 3, 4, 5, 6,
                7, 8, 9, 10, 11, 12, 13, 15, 14, }));
        assertEquals(-1, SymmetricGroupUtil.sgn(new int[] { 6, 12, 9, 2, 15, 1,
                14, 11, 13, 8, 3, 5, 4, 10, 7, }));

        try {
            assertEquals(1, SymmetricGroupUtil.sgn(new int[] {}));
            fail();
        } catch (IllegalArgumentException e) {
        }
    }

    public void testIsPermutation() {
        assertEquals(false, SymmetricGroupUtil.isPermutation(new int[] {}));
        assertEquals(true, SymmetricGroupUtil.isPermutation(new int[] { 1, }));
        assertEquals(true, SymmetricGroupUtil.isPermutation(new int[] { 1, 2,
                3, }));
        assertEquals(true, SymmetricGroupUtil.isPermutation(new int[] { 1, 3,
                2, }));
        assertEquals(true, SymmetricGroupUtil.isPermutation(new int[] { 3, 7,
                1, 2, 5, 8, 4, 6, }));
        assertEquals(true, SymmetricGroupUtil.isPermutation(new int[] { 1, 2,
                3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, }));
        assertEquals(true, SymmetricGroupUtil.isPermutation(new int[] { 1, 2,
                3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 14, }));

        assertEquals(false, SymmetricGroupUtil.isPermutation(new int[] { 0, }));
        assertEquals(false, SymmetricGroupUtil.isPermutation(new int[] { 2, }));
        assertEquals(false, SymmetricGroupUtil
                .isPermutation(new int[] { 1, 1, }));
        assertEquals(false, SymmetricGroupUtil
                .isPermutation(new int[] { 1, 3, }));

    }

    public void testGetPermutation() {
        assertEquals(true, SymmetricGroupUtil.isPermutation(SymmetricGroupUtil
                .getPermutation(1)));
        assertEquals(true, SymmetricGroupUtil.isPermutation(SymmetricGroupUtil
                .getPermutation(15)));
        System.out.println("---");
        System.out.println(Arrays.toString(SymmetricGroupUtil
                .getPermutation(15)));
        System.out.println(Arrays.toString(SymmetricGroupUtil
                .getPermutation(15)));
        System.out.println(Arrays.toString(SymmetricGroupUtil
                .getPermutation(15)));
        System.out.println(Arrays.toString(SymmetricGroupUtil
                .getPermutation(15)));

        try {
            assertEquals(true, SymmetricGroupUtil
                    .isPermutation(SymmetricGroupUtil.getPermutation(0)));
            fail();
        } catch (IllegalArgumentException e) {
        }

    }
}
---



[Android][AppInventor] MoleMash の Java 版 (2) SurfaceView 版

2012年01月05日 | Android
[Android][AppInventor] MoleMash の Java 版 (2) SurfaceView 版
================================================================================

AppInventor のチュートリアルのサンプル「MoleMash」を Java を使って実装してみる。

MoleMash は以下のようなアプリ。
---
MoleMash は、ゲーム場のランダムな位置にひょっこり現れた一匹のモグラを、消えてし
まわないうちに叩いてポイントを稼ぐゲームです。
---
(ソフトウェア技術ドキュメントを勝手に翻訳 - AppInventor チュートリアル、
http://www.techdoctranslator.com/appinventor/learn/tutorials より)

ただし、オリジナルは、画面の縦横が変わっても点数が保持されているが、これは保持し
ていない。

サンプルの (1) は、マルチスレッドを使っていなかったので、モグラを描画するタイミ
ングとタッチするタイミングが合わず、うまく得点できなかった。このため、タッチされ
た、と判定できる領域を拡大して対応した。また、リセットボタンの押下のタイミングも
合わなかった。
このサンプルは、SurfaceView クラスを使うことで、描画するのを別スレッドで実行でき
る。これにより、描画とタッチのタイミングのずれをなくすことができる。

今回作ったクラスはつぎの 4 つ。ただし、(*)のものは、前回のサンプル1と同じである。
    MoleMashActivity    アクティビティ(*)
    CanvasView          カスタム・ビュー(このクラスで描画を制御)
    Mole                もぐら(*)
    MoleTimer           もぐらを定期的に描画するスレッド


■ カスタム・ビュー

描画のキャンパスになっているカスタム・ビューのクラス。
SurfaceView クラスを継承する。
また、SurfaceHolder.Callback インターフェースを実装することで、SurfaceView オブ
ジェクトの変更に関する情報を受け取ることができる。

□ CanvasView.java
---
package jp.marunomaruno.android.molemash;

import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.TextView;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * モグラを出現させてたたくキャンバス。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class CanvasView extends SurfaceView implements SurfaceHolder.Callback { 
// (1)

    private Mole mole; // モグラ 
    private Thread drawing;    // タイマー    // (2)

    public CanvasView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize(context);
    }

    public CanvasView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize(context);
    }

    public CanvasView(Context context) {
        super(context);
        initialize(context);
    }

    /**
     * 初期化する。
     * @param context
     */
    private void initialize(Context context) {
        // サーフェースホルダーを設定する
        getHolder().addCallback(this);    // (3)

        // タッチ領域のスケールを指定する
        float scale = Math
                .abs((getResources().getInteger(R.integer.scaleRate) - 100) 
                        / 100F);

        // モグラを生成する
        mole = new Mole(BitmapFactory.decodeResource(context.getResources(),
                R.drawable.mole), scale);
    }

    /*
     * (non-Javadoc)
     * @see android.view.View.OnTouchListener#onTouch(android.view.View,
     * android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            PointF touchPoint = new PointF(event.getX(), event.getY());

            // タッチされたら、加点する
            if (isTouch(touchPoint, mole)) {
                // 得点をカウントアップして表示する
                TextView scoreView = (TextView) ((Activity) getContext())
                        .findViewById(R.id.score);
                scoreView.setText(String.format("%d", mole.scoreUp()));

                // 実機を振動する
                Vibrator vibrator = (Vibrator) getContext().getSystemService(
                        Context.VIBRATOR_SERVICE);
                vibrator.vibrate(getResources().getInteger(R.integer.vibMillis))
;
            }
            invalidate();
            break;

        default:
            assert true; // 何もしない
            break;

        }

        return true;
    }

    private boolean isTouch(PointF touchPoint, Mole mole) {
        RectF rect = mole.getMoleRect();

        Log.d("TEST", String.format("[%.1f, %.1f] in [%s]: %b", touchPoint.x,
                touchPoint.y, rect.toString(), rect.contains(touchPoint.x,
                        touchPoint.y)));

        return rect.contains(touchPoint.x, touchPoint.y);
    }

    
    public Mole getMole() {
        return mole;
    }

    /**
     * スコアをリセットする。
     */
    public void resetScore() {
        Log.d("TEST", String.format("  - RESET"));

        TextView scoreView = (TextView) ((Activity) getContext())
                .findViewById(R.id.score);
        scoreView.setText(String.format("%d", mole.resetScore()));
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {    // (4)
        Log.d("TEST", "SurfaceHolder.Callback.surfaceChanged()");
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {    // (5)
        Log.d("TEST", "SurfaceHolder.Callback.surfaceCreated()");

        // 描画スレッドを起動する
        drawing = new MoleTimer(this);    // (6)
        drawing.start();    // (7)
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {    // (8)
        Log.d("TEST", "SurfaceHolder.Callback.surfaceDestroyed()");
        
        // 描画スレッドを停止する
        drawing = null;    // (9)
    }

}
---

(1) SurfaceView クラスを継承、SurfaceHolder.Callback インターフェースを実装

    public class CanvasView extends SurfaceView implements 
                SurfaceHolder.Callback {    // (1)


□ SurfaceView クラス

アプリケーションのスレッドと描画のスレッドを独立して作ることができるようにする 
View のクラス。処理とは別に独立したスレッドで描画ができるので、定期的な再描画が
必要なアプリケーションに向いている。

・クラス階層
java.lang.Object
   +  android.view.View
         +  android.view.SurfaceView

・主なメソッド
---
void           draw(Canvas canvas)
SurfaceHolder  getHolder()
---


□ SurfaceHolder.Callback インターフェース

SurfaceView オブジェクトの変更に関する情報を受け取るためのインターフェース

・メソッド
---
void  surfaceChanged(SurfaceHolder holder, int format, int width, int height)
      SurfaceView オブジェクの形式やサイズが変更になったときに呼び出される

void  surfaceCreated(SurfaceHolder holder)
      SurfaceView オブジェクが作成された直後に呼び出される

void  surfaceDestroyed(SurfaceHolder holder)
      SurfaceView オブジェクが破棄される直前に呼び出される
---
SurfaceHolder holder SurfaceView オブジェクの情報


(2) 指定されたミリ秒数分、描画するオブジェクト

    private Thread drawing;    // タイマー    // (2)

タイマー・スレッド(MoleTimer.java)を格納する。


(3) サーフェースホルダーを設定する

サーフェースホルダー・オブジェクトに、コールバック・メソッドのあるオブジェクトを
追加する。

    getHolder().addCallback(this);    // (3)

このオブジェクト自身が、SurfaceHolder.Callback インターフェースを実装したコール
バック・メソッドのあるオブジェクトなので、自分自身(this)を渡す。


□ SurfaceHolder インターフェース

サーフェースホルダーは、サーフェース・オブジェクトの情報を持ったオブジェクト。
このビューに変更があった場合などに対する処理を規定するコールバックメソッドは、
SurfaceHolder.Callback インターフェースを実装したオブジェクトとして定義する。

・主なメソッド
---
void     addCallback(SurfaceHolder.Callback callback)
    コールバックを追加する

void     removeCallback(SurfaceHolder.Callback callback)
    コールバックを削除する

Canvas     lockCanvas(Rect dirty)
Canvas     lockCanvas()
    描画を開始する(ダブルバッファリングのロック)

void     unlockCanvasAndPost(Canvas canvas)
    描画を終了する(ダブルバッファリングのアンロック)
---


(4) SurfaceView オブジェクの形式やサイズが変更になったときに呼び出されるメソッド
の定義

    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {    // (4)


(5) SurfaceView オブジェクが作成された直後に呼び出されるメソッドの定義

    public void surfaceCreated(SurfaceHolder holder) {    // (5)

このメソッドで、描画するスレッドを起動する。


(6)(7) 描画スレッドを起動する

    drawing = new MoleTimer(this);    // (6)
    drawing.start();    // (7)


(8) SurfaceView オブジェクが破棄される直前に呼び出されるメソッドの定義

    public void surfaceDestroyed(SurfaceHolder holder) {    // (8)

このメソッドで、(5)で起動した描画スレッドを停止する。


(9) 描画スレッドを停止する

    drawing = null;    // (9)


■ タイマー・スレッド

定期的にもぐらを描画する。描画するのは、プロパティで指定した時間(ミリ秒)分である。
この部分は、アプリケーションとは別のスレッドとして動作する。

□ MoleTimer.java
---
package jp.marunomaruno.android.molemash;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.util.Log;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * もぐらを表示するスレッド。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class MoleTimer extends Thread {    // (1)

    private CanvasView view;

    public MoleTimer(CanvasView view) {
        this.view = view;
    }

    @Override
    public void run() {    // (2)
        Log.d("TEST", "MoleTimer.run()");
        
        while (true) {
            Canvas canvas = view.getHolder().lockCanvas();    // (3)

            if (canvas != null) {
                moveMole(canvas);    // (4)
                view.getHolder().unlockCanvasAndPost(canvas);    // (5)
            }

            try {
                Thread.sleep(view.getContext().getResources().getInteger(
                        R.integer.appearanceMillis));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void moveMole(Canvas canvas) {
        assert canvas != null;
        
        Mole mole = view.getMole();
        canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 
                                            // キャンバスをクリアする    // (6)
        setAppearancePoint(mole); // 出現座標を設定する
        canvas.drawBitmap(mole.getPicture(), mole.getX(), mole.getY(), null);
    }

    /**
     * もぐらの出現座標を設定する。
     * @return 座標設定後のモグラ
     */
    private Mole setAppearancePoint(Mole mole) {
        // タッチ領域のスケールを指定する
        float scale = Math.abs((view.getContext().getResources().getInteger(
                R.integer.scaleRate) - 100) / 100F);

        // タッチ領域がすべて画面に入るように座標を設定する
        PointF point = new PointF();
        point.x = (float) (Math.random()
                * (view.getWidth() - mole.getPicture().getWidth()
                        * (1 + scale * 2)) + mole.getPicture().getWidth()
                * scale);
        point.y = (float) (Math.random()
                * (view.getHeight() - mole.getPicture().getHeight()
                        * (1 + scale * 2)) + mole.getPicture().getHeight()
                * scale);

        mole.setPoint(point);
        Log.d("TEST", mole.toString());

        return mole;
    }

}
---


(1) Thread クラスを継承

    public class MoleTimer extends Thread {    // (1)


(2) run() メソッドのオーバーライド

    public void run() {    // (2)


(3) 描画を開始する

    Canvas canvas = view.getHolder().lockCanvas();    // (3)

ダブルバッファリングをするために、キャンバスをロックする。


(4) もぐらを出現

つぎの private メソッドで、指定されたキャンバスに描画する。

    moveMole(canvas);    // (4)


(5) 描画を終了する

    view.getHolder().unlockCanvasAndPost(canvas);    // (5)

指定されたキャンバスのロックを解除する。


(6) キャンバスをクリアする

    canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); // (6)

キャンバスをクリアしないと、ループの中で前に描画されたモグラが残ったままになる。
キャンバスをクリアするには、上記のように、drawColor() メソッドで、黒くキャンバス
を塗りつぶす。

PorterDuff クラスは、「Porter-Duffのルール」を記述しているクラス。Porter-Duffの
ルールは、2 つのイメージを合成する場合の 12 のルールを規定している。

この中で、結合イメージには何も描画しない、というのがつぎの定数になっている。
    PorterDuff.Mode.CLEAR

参考URL:
Merlinの魔術: Porter-Duffのルール!
http://www.ibm.com/developerworks/jp/java/library/j-mer0918/

Compositing Degital Images
http://www.keithp.com/~keithp/porterduff/p253-porter.pdf

    ----------- --------------- -------- -------
    操作        quadruple(4つ)  Fa       Fb
    ----------- --------------- -------- -------
    clear       (0, 0, 0, 0)    0        0
    A           (0, A, 0, A)    1        0
    B           (0, 0, B, B)    0        1
    A over B    (0, A, B, A)    1        1-δa
    B over A    (0, A, B, B)    1-δb    1
    A in B      (0, 0, 0, A)    δb      0
    B in A      (0, 0, 0, B)    0        δa
    A out B     (0, A, 0, 0)    1-δb    0
    B out A     (0, 0, B, 0)    0        1-δa
    A atop B    (0, 0, B, A)    δb      1-δa
    B atop A    (0, A, 0, B)    1-δb    δa
    A xor B     (0, A, B, 0)    1-δb    1-δa
    ----------- --------------- -------- -------
    「Compositing Degital Images」より抜粋

A が SRC、B が DST とすればよい。
quadruple の部分は、2 次元の正方行列で考えたほうがわかりやすい。


■ 前回(1) と同じコードだが、再掲する。

□ MoleMashActivity
---
package jp.marunomaruno.android.molemash;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * アクティビティ。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class MoleMashActivity extends Activity {
    private CanvasView canvasView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onResume() {
        Log.d("TEST", "MoleMashActivity.onResume()");
        super.onResume();

        canvasView = (CanvasView) findViewById(R.id.canvasView);
    }

    /**
     * 「リセット」ボタン・クリック
     * @param v
     */
    public void onResetButtonClick(View v) {
        Log.d("TEST", String.format(
                "MoleMashActivity.onResetButtonClick() - [%s]", v.toString()));
        canvasView.resetScore();    // (1)
    }

}
---


□ Mole.java
---
package jp.marunomaruno.android.molemash;

import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.RectF;

/**
 * AppInventor のサンプル MoleMash の Java 版。
 * もぐら。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class Mole {

    private Bitmap picture;    // 画像    // (1)
    private static int score;    // スコア(すべてのモグラで共通)    // (2)
    private PointF point;    // 出現ポイント    // (3)
    private float scale;    // タッチされる領域のスケール    // (4)

    public Mole(Bitmap picture, float scale) {
        this.picture = picture;
        this.point = new PointF();
        this.scale = scale;
        resetScore();
    }

    public Bitmap getPicture() {
        return picture;
    }

    public int getScore() {
        return score;
    }

    
    public PointF getPoint() {
        return point;
    }

    public void setPoint(PointF point) {
        this.point = point;
    }

    /**
     * 出現ポイントのX座標を返す。
     * @return 出現ポイントのX座標
     */
    public float getX() {
        return point.x;
    }

    /**
     * 出現ポイントのY座標を返す。
     * @return 出現ポイントのY座標
     */
    public float getY() {
        return point.y;
    }

    /**
     * スコアをリセットする。
     * @return リセット後のスコア
     */
    public int resetScore() {
        score = 0;
        return score;
    }

    /**
     * スコアに加点する。
     * @return 加点後のスコア
     */
    public int scoreUp() {
        score++;
        return score;
    }

    /**
     * タッチ領域を返す。
     * @return タッチ領域
     */
    public RectF getMoleRect() {
        // タッチ領域を、スケールを考慮して設定する
        RectF rect = new RectF(point.x - picture.getWidth() * scale, point.y
                - picture.getHeight() * scale, point.x + picture.getWidth()
                * (1 + scale), point.y + picture.getHeight() * (1 + scale)); // (5)

        return rect;
    }

    @Override
    public String toString() {
        return String
                .format("Mole=[%d, [%.1f, %.1f], %.1f]", score, point.x, point.y,
                         scale);
    }
}
---



[Android][AppInventor] MoleMash の Java 版 (1) 失敗例

2012年01月04日 | Android
[Android][AppInventor] MoleMash の Java 版 (1) 失敗例
================================================================================

AppInventor のチュートリアルのサンプル「MoleMash」を Java を使って実装してみる。
★今回のアプリケーションは失敗例★

MoleMash は以下のようなアプリ。
---
MoleMash は、ゲーム場のランダムな位置にひょっこり現れた一匹のモグラを、消えてし
まわないうちに叩いてポイントを稼ぐゲームです。
---
(ソフトウェア技術ドキュメントを勝手に翻訳 - AppInventor チュートリアル、
http://www.techdoctranslator.com/appinventor/learn/tutorials より)

ただし、オリジナルは、画面の縦横が変わっても点数が保持されているが、これは保持し
ていない。

【注意】今回のサンプルはよくないので、これをもとに実際のアプリケーションとして組
み立ててはいけない。これだと、タッチするタイミングと描画するタイミングがうまく合
わない。
実際は、SurfaceView を使って、マルチスレッドとして作るべきである(次のサンプル参
照)。


今回作ったクラスはつぎの 3 つ。

MoleMashActivity    アクティビティ
CanvasView          カスタム・ビュー(このクラスで描画)
Mole                もぐら


■ アクティビティ

□ MoleMashActivity
---
package jp.marunomaruno.android.molemash;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * アクティビティ。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class MoleMashActivity extends Activity {
    private CanvasView canvasView;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onResume() {
        Log.d("TEST", "MoleMashActivity.onResume()");
        super.onResume();

        canvasView = (CanvasView) findViewById(R.id.canvasView);

    }

    /**
     * 「リセット」ボタン・クリック
     * @param v
     */
    public void onResetButtonClick(View v) {
        Log.d("TEST", String.format(
                "MoleMashActivity.onResetButtonClick() - [%s]", v.toString()));
        canvasView.resetScore();    // (1)
    }
}
---

(1) スコアのリセットの処理

    canvasView.resetScore();    // (1)


■ カスタム・ビュー

描画のキャンパスになっているカスタム・ビューのクラス。

□ CanvasView.java
---
package jp.marunomaruno.android.molemash;

import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * AppInventor のサンプル MoleMash の Java 版。 
 * モグラを出現させてたたくキャンバス。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class CanvasView extends ImageView {

    private Mole mole; // モグラ // (1)

    public CanvasView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initialize(context);
    }

    public CanvasView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialize(context);
    }

    public CanvasView(Context context) {
        super(context);
        initialize(context);
    }

    /**
     * 初期化する。
     * 
     * @param context
     */
    private void initialize(Context context) {
        // タッチ領域のスケールを指定する
        float scale = Math
                .abs((getResources().getInteger(R.integer.scaleRate) - 100) 
                        / 100F); // (2)

        // モグラを生成する
        mole = new Mole(BitmapFactory.decodeResource(context.getResources(),
                R.drawable.mole), scale); // (3)
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        setAppearancePoint(mole); // 出現座標を設定する
        canvas.drawBitmap(mole.getPicture(), mole.getX(), mole.getY(), null);

        invalidate(); // (4)

        try {
            Thread.sleep(getResources().getInteger(R.integer.appearanceMillis));
                                                                         // (5)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * もぐらの出現座標を設定する。
     * @return 座標設定後のモグラ
     */
    private Mole setAppearancePoint(Mole mole) {
        // タッチ領域のスケールを指定する
        float scale = Math
                .abs((getResources().getInteger(R.integer.scaleRate) - 100) 
                        / 100F); // (2)

        // タッチ領域がすべて画面に入るように座標を設定する
        PointF point = new PointF();
        point.x = (float) (Math.random()
                * (getWidth() - mole.getPicture().getWidth() * (1 + scale * 2)) 
                        + mole
                .getPicture().getWidth()
                * scale); // (6)
        point.y = (float) (Math.random()
                * (getHeight() - mole.getPicture().getHeight()
                        * (1 + scale * 2)) + mole.getPicture().getHeight()
                * scale);

        mole.setPoint(point);
        Log.d("TEST", mole.toString());

        return mole;
    }

    /*
     * (non-Javadoc)
     * @see android.view.View.OnTouchListener#onTouch(android.view.View,
     * android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: // (7)
            PointF touchPoint = new PointF(event.getX(), event.getY());

            // タッチされたら、加点する
            if (isTouch(touchPoint, mole)) {
                // 得点をカウントアップして表示する
                TextView scoreView = (TextView) ((Activity) getContext())
                        .findViewById(R.id.score); // (8)
                scoreView.setText(String.format("%d", mole.scoreUp())); // (9)

                // 実機を振動する
                Vibrator vibrator = (Vibrator) getContext().getSystemService(
                        Context.VIBRATOR_SERVICE); // (10)
                vibrator.vibrate(getResources().getInteger(R.integer
                        .vibMillis)); // (11)
            }

            invalidate(); // (12)
            break;

        default:
            assert true; // 何もしない
            break;

        }

        return true;
    }

    private boolean isTouch(PointF touchPoint, Mole mole) {
        RectF rect = mole.getMoleRect();

        Log.d("TEST", String.format("[%.1f, %.1f] in [%s]: %b", touchPoint.x,
                touchPoint.y, rect.toString(), rect.contains(touchPoint.x,
                        touchPoint.y)));

        return rect.contains(touchPoint.x, touchPoint.y); // (13)
    }

    /**
     * スコアをリセットする。
     */
    public void resetScore() {
        Log.d("TEST", String.format("  - RESET"));

        TextView scoreView = (TextView) ((Activity) getContext())
                .findViewById(R.id.score);
        scoreView.setText(String.format("%d", mole.resetScore())); // (14)
    }

}
---

(1) モグラのオブジェクト

    private Mole mole;    // モグラ    // (1)


(2) タッチ領域のスケールを指定する

実際の指定は、res/values/properties.xml の scaleRate の要素の値。
これは、図形(モグラ)の枠の幅・高さを何%のスケールで拡大・縮小するか、という指定
になっている。
これを、扱いやすいように、変換する。
変換後の値は、基の図形に対する追加分のスケール。

    float scale = Math
        .abs((getResources().getInteger(R.integer.scaleRate) - 100) / 100F);// (2)

100 が指定してあると、この scale は 0.0 になり、基の図形の大きさと変わらない。
200 が指定してあると、この scale は 1.0 になり、基の図形の大きさに対して、たて、
横がそれぞれの方向に 1 つ分大きくなる。すなわち、面積としては 9 倍になる。


(3) モグラを生成する

図形とスケールを指定して、モグラのオブジェクトを生成する。

    mole = new Mole(BitmapFactory.decodeResource(context.getResources(),
            R.drawable.mole), scale);    // (3)


(4) 再描画する

    invalidate();    // (4)


(5) モグラの出現時間

モグラの出現時間分、スリープする。

    Thread.sleep(getResources()
            .getInteger(R.integer.appearanceMillis));    // (5)


(6) タッチ領域がすべて画面に入るように座標を設定する
    point.x = (float) (Math.random()
          * (getWidth() - mole.getPicture().getWidth() * (1 + scale * 2)) + mole
          .getPicture().getWidth()
          * scale);    // (6)


(7)(8)(9) タッチされたら、加点する

タッチされたときに、モグラにタッチしたかどうかを判断する。

    case MotionEvent.ACTION_DOWN: // (7)


モグラにタッチされれば、得点を表示するビューに、カウントアップした得点を設定する。

得点を表示するビューは、アクティビティが持っているので、ここから findViewById() 
メソッドでそのオブジェクトの参照を取得する。
なお、このカスタム・ビューには、Activity 型でなく、そのスーパークラスである Conte
xt 型で保持しているので、 Activity 型にキャストしないと findViewById() メソッド
が使えない。

    TextView scoreView = (TextView) ((Activity) context)
            .findViewById(R.id.score);    // (8)

得点をカウントアップする。

    scoreView.setText(String.format("%d", mole.scoreUp()));    // (9)


(10)(11) 実機を振動させる

実機を振動させるには、Vibrator クラスの vibrate() メソッドを使う。
Vibrator クラスのオブジェクトは、以下のように、getSystemService() メソッドを使う。

    Vibrator vibrator = (Vibrator) context.getSystemService(
            Context.VIBRATOR_SERVICE); // (10)

vibrate() メソッドで、振動させる時間を指定する。

    vibrator.vibrate(getResources().getInteger(R.integer.vibMillis));    // (11)

□ Vibrator クラス


java.lang.Object
   +     android.os.Vibrator


□ メソッド
---
void     cancel()                             振動をやめる
boolean  hasVibrator()                        バイブレーターを持っているかどうか
void     vibrate(long[] pattern, int repeat)  パターンにしたがって振動する
void     vibrate(long milliseconds)           指定時間振動する
---


(12) 再描画

    invalidate();    // (12)


(13) タッチされた点が図形の四角形に入っているかどうかの判断

図形のタッチ領域の中に実際にタッチされた座標が入っているかどうかを判断する。
タッチ領域は、図形(モグラ)を基本にして、スケールによって調整されたものである。
タッチ領域は、Mole クラスの getMoleRect() メソッドで取得する。

    return rect.contains(touchPoint.x, touchPoint.y);    // (13)


(14) スコアをリセットする。

    scoreView.setText(String.format("%d", mole.resetScore()));    // (14)


■ データ

描画するデータを保持するモグラ・クラス。
つぎのデータを持つ。
    ・画像
    ・スコア
    ・出現ポイント
    ・タッチされる領域のスケール

スコアは、クラス変数として、すべてのモグラ・オブジェクトで共通にしている。今回は
モグラは 1 匹しか出現させないが、ゲームによっては、2 匹以上を出現させうることを
考慮して、共通で点数を持つ可能性があるので、クラス変数としている。

タッチされる領域のスケールは、本来必要ない物であるが、この失敗例では、タッチとモ
グラ出現タイミングが合わず、タッチされたことを認識する領域を拡大させているので、
このスケールも持つことにした。
今後は、ゲームの難易度の調整に使用できる。


□ Mole.java
---
package jp.marunomaruno.android.molemash;

import android.graphics.Bitmap;
import android.graphics.PointF;
import android.graphics.RectF;

/**
 * AppInventor のサンプル MoleMash の Java 版。
 * もぐら。
 * @author marunomaruno
 * @version 1.0, 2011-12-31
 */
public class Mole {

    private Bitmap picture;    // 画像    // (1)
    private static int score;    // スコア(すべてのモグラで共通)    // (2)
    private PointF point;    // 出現ポイント    // (3)
    private float scale;    // タッチされる領域のスケール    // (4)

    public Mole(Bitmap picture, float scale) {
        this.picture = picture;
        this.point = new PointF();
        this.scale = scale;
        resetScore();
    }

    public Bitmap getPicture() {
        return picture;
    }

    public int getScore() {
        return score;
    }

    
    public PointF getPoint() {
        return point;
    }

    public void setPoint(PointF point) {
        this.point = point;
    }

    /**
     * 出現ポイントのX座標を返す。
     * @return 出現ポイントのX座標
     */
    public float getX() {
        return point.x;
    }

    /**
     * 出現ポイントのY座標を返す。
     * @return 出現ポイントのY座標
     */
    public float getY() {
        return point.y;
    }

    /**
     * スコアをリセットする。
     * @return リセット後のスコア
     */
    public int resetScore() {
        score = 0;
        return score;
    }

    /**
     * スコアに加点する。
     * @return 加点後のスコア
     */
    public int scoreUp() {
        score++;
        return score;
    }

    /**
     * タッチ領域を返す。
     * @return タッチ領域
     */
    public RectF getMoleRect() {
        // タッチ領域を、スケールを考慮して設定する
        RectF rect = new RectF(point.x - picture.getWidth() * scale, point.y
                - picture.getHeight() * scale, point.x + picture.getWidth()
                * (1 + scale), point.y + picture.getHeight() * (1 + scale));    
                                                                    // (5)

        return rect;
    }

    @Override
    public String toString() {
        return String
                .format("Mole=[%d, [%.1f, %.1f], %.1f]", score, point.x, point.y,
 scale);
    }
}
---

(1)-(4) 変数

Mole オブジェクトが保持する変数。

    private Bitmap picture;    // 画像    // (1)
    private PointF point;    // 出現ポイント    // (3)
    private float scale;    // タッチされる領域のスケール    // (4)

スコアは、今後、モールの数が増えて表示させたときに備えて、static としておく。

    private static int score;    // スコア(すべてのモグラで共通)    // (2)


(5) タッチ領域を、スケールを考慮して設定する

図形の描画領域を中心に、このスケール分を追加した領域を設定する。

    RectF rect = new RectF(point.x - picture.getWidth() * scale, point.y
            - picture.getHeight() * scale, point.x + picture.getWidth()
            * (1 + scale), point.y + picture.getHeight() * (1 + scale));  // (5)

scale = 0.0 のときは、 (x, y, x + w, y + h) の領域、すなわち、図形の大きさ自身
scale = 1.0 のときは、 (x - w, y - h, x + w * 2, y + h * 2) の領域、すなわち、図
形 9 つ分の大きさ


■ レイアウト

□ res/layout/main.xml
---

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    
    <jp.marunomaruno.android.molemash.CanvasView
        android:id="@+id/canvasView"
        android:layout_width="300px"
        android:layout_height="300px" />

    <TextView
        android:id="@+id/score"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <Button
        android:id="@+id/resetButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onResetButtonClick"
        android:text="@string/resetButton" />

</LinearLayout>
---


■ 設定データ

□ res/values/strings.xml
---

<resources>
    <string name="app_name">MoleMash</string>
    <string name="resetButton">リセット</string>
</resources>
---


出現時間などを設定値として記述する。

□ res/values/properties.xml
---

<resources>
    

    <integer name="appearanceMillis">1000</integer>    
        
    
    <integer name="scaleRate">200</integer>    

    
    <integer name="vibMillis">300</integer>    
</resources>
---

(1) 出現時間(ms)

    <integer name="appearanceMillis">1000</integer>    

1000 は、デバッグ用。リリース用は 500 にする。

        
(2) タッチする領域の大きさ(基絵に対する%)

    <integer name="scaleRate">200</integer>    

基本図形に対して上下左右に1つ分拡大する。すなわち、9 倍の大きさ。


(3) 振動させる時間(ミリ秒)

    <integer name="vibMillis">300</integer>    

得点したときに実機を振動させる時間。


■ マニュフェスト

実機を振動させるためには、許可が必要。

□ AndroidManifest.xml
---

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.marunomaruno.android.molemash"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />
    <uses-permission android:name="android.permission.VIBRATE"/>    

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".MoleMashActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
---

(1) 実機の振動を許可

    <uses-permission android:name="android.permission.VIBRATE"/>