[Android][AppInventor] PaintPot の Java 版 ================================================================================ AppInventor のチュートリアルのサンプル「PaintPot」を Java を使って実装してみる。 PaintPot は以下のようなアプリ。 --- PaintPot で画面をタッチしてさまざまなカラーのドットや線を描いてらくがきができま す。 これが PaintPot アプリでできることです。 描きたい色の仮想ペンキ入れに、指を浸します。 画面にドラッグした指にそって線が描かれます。 画面を突いてドットを描きます。 下のボタンを使って画面をきれいに拭き取ります。 描画する背景としてイメージが含まれています --- (ソフトウェア技術ドキュメントを勝手に翻訳 - AppInventor チュートリアル、 http://www.techdoctranslator.com/appinventor/learn/tutorials より) ただし、オリジナルは、画面の縦横が変わっても線が保持されているが、これは保持して いない。 今回作ったクラスはつぎの 4 つ。 PaintPotActivity アクティビティ CanvasView カスタム・ビュー(このクラスで描画) PointWithPaint Paint オブジェクト付きの点 PathWithPaint Paint オブジェクト付きの折れ線 今回作ってみて、GUI 系のことは、AppInventor のようなものがあると便利。Java で作 るとなると、わりと面倒、という感じだ。 ■ アクティビティ 線の色や太さを変えるボタンとその処理。 今回は、レイアウトの属性 android:onClick 属性にハンドラーを設定したので、このク ラスの中でハンドラーを定義している。 □ PaintPotActivity --- package jp.marunomaruno.android.paintpot; import android.app.Activity; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.view.View; /** * AppInventor のサンプル PaintPot の Java 版。 * アクティビティ * @author marunomaruno * @version 1.0, 2011-12-25 */ public class PaintPotActivity extends Activity { private CanvasView canvasView; // (1) /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { Log.d("TEST", "PaintPotActivity.onCreate()"); super.onCreate(savedInstanceState); setContentView(R.layout.main); } @Override protected void onResume() { Log.d("TEST", "PaintPotActivity.onResume()"); super.onResume(); canvasView = (CanvasView) findViewById(R.id.canvasView); } /** * 「赤」ボタン・クリック * @param v */ public void onRedButtonClick(View v) { Log.d("TEST", String.format( "PaintPotActivity.onRedButtonClick() - [%s]", v.toString())); canvasView.setPaintColor(Color.RED); // (2) } /** * 「緑」ボタン・クリック * @param v */ public void onGreenButtonClick(View v) { Log.d("TEST", String.format( "PaintPotActivity.onGreenButtonClick() - [%s]", v.toString())); canvasView.setPaintColor(Color.GREEN); // (3) } /** * 「青」ボタン・クリック * @param v */ public void onBlueButtonClick(View v) { Log.d("TEST", String.format( "PaintPotActivity.onBlueButtonClick() - [%s]", v.toString())); canvasView.setPaintColor(Color.BLUE); // (4) } /** * 「クリア」ボタン・クリック * @param v */ public void onWipeButtonClick(View v) { Log.d("TEST", String.format( "PaintPotActivity.onWipeButtonClick() - [%s]", v.toString())); canvasView.wipe(); // (5) } /** * 「太線」ボタン・クリック * @param v */ public void onHeavyButtonClick(View v) { Log.d("TEST", String.format( "PaintPotActivity.onHeavyButtonClick() - [%s]", v.toString())); canvasView.setStrokeWidth(CanvasView.HEAVY); // (6) } /** * 「細線」ボタン・クリック * @param v */ public void onThinButtonClick(View v) { Log.d("TEST", String.format( "PaintPotActivity.onThinButtonClick() - [%s]", v.toString())); canvasView.setStrokeWidth(CanvasView.THIN); // (7) } } --- (1) カスタムビューのオブジェクト ボタンをクリックしたときに、色を変えたり、線の太さを変えたりするために持っている。 private CanvasView canvasView; // (1) (2)-(7) カスタムビューの属性設定 それぞれ、ボタンをクリックしたときに行う処理。 canvasView.setPaintColor(Color.RED); // (2) canvasView.setPaintColor(Color.GREEN); // (3) canvasView.setPaintColor(Color.BLUE); // (4) canvasView.wipe(); // (5) canvasView.setStrokeWidth(CanvasView.HEAVY); // (6) canvasView.setStrokeWidth(CanvasView.THIN); // (7) ■ カスタム・ビュー 描画のキャンパスになっているカスタム・ビューのクラス。 丸や線のデータをリストで保持している。 □ CanvasView.java --- package jp.marunomaruno.android.paintpot; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.ImageView; /** * AppInventor のサンプル PaintPot の Java 版。 * 丸を打つ、線を引くキャンバス。 * @author marunomaruno * @version 1.0, 2011-12-25 */ public class CanvasView extends ImageView { public static final float HEAVY = 15F; // 太い // (1) public static final float THIN = 5F; // 細い // (2) private Paint paint; private List<PointWithPaint> dots; // ドット用 // (3) private List<PathWithPaint> lines; // 軌跡用 // (4) public CanvasView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } public CanvasView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CanvasView(Context context) { super(context); init(context); } /** * 初期化する。 * @param context */ private void init(Context context) { paint = new Paint(); paint.setStrokeWidth(THIN); paint.setAntiAlias(true); wipe(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 丸を描画する for (PointWithPaint point : dots) { Log.v("TEST", "dot = " + point.toString()); canvas.drawCircle(point.x, point.y, point.paint.getStrokeWidth(), point.paint); } // 線を描画する for (PathWithPaint line : lines) { Log.v("TEST", "line = " + line.toString()); canvas.drawPath(line.path, line.paint); } } // onTouchEvent() だけで使用 private int prevAction = -1; // 直前のアクション // (5) private Path path = null; // パス // (6) /* * (non-Javadoc) * @see android.view.View.OnTouchListener#onTouch(android.view.View, * android.view.MotionEvent) */ @Override public boolean onTouchEvent(MotionEvent event) { Log.d("TEST", String.format( "CanvasTouchlistener.onTouch() - action = [%d]", event .getAction())); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // (7) path = new Path(); path.moveTo(event.getX(), event.getY()); break; case MotionEvent.ACTION_UP: // (8) if (prevAction == MotionEvent.ACTION_DOWN) { // (9) dots.add(new PointWithPaint(event.getX(), event.getY(), paint)); } lines.add(new PathWithPaint(path, paint)); invalidate(); break; case MotionEvent.ACTION_MOVE: // (10) path.lineTo(event.getX(), event.getY()); break; default: assert true; // 何もしない break; } prevAction = event.getAction(); // (11) return true; // (12) } /** * 指定された色に設定する。 * @param color 色 */ public void setPaintColor(int color) { paint.setColor(color); } /** * 指定された線の幅にする。 * @param width 幅 */ public void setStrokeWidth(float width) { paint.setStrokeWidth(width); } /** * 描画したものをきれいにする。 */ public void wipe() { dots = new ArrayList<PointWithPaint>(); lines = new ArrayList<PathWithPaint>(); invalidate(); } } --- (1)(2) 線の太さをあらわす定数 public static final float HEAVY = 15F; // 太い // (1) public static final float THIN = 5F; // 細い // (2) (3)(4) 軌跡の保持 private List<PointWithPaint> dots; // ドット用 // (3) private List<PathWithPaint> lines; // 折れ線用 // (4) (5)(6) onTouchEvent() だけで使用するインスタンス変数 ACTION_UP したときにサークルを描くと、ドラッグしているときに描いた折れ線の最後に サークルが描かれるので、前回のアクションを保持しておくための変数。 private int prevAction = -1; // 直前のアクション // (5) 1回の移動で描かれる折れ線を保持する。 private Path path = null; // パス // (6) (7) タッチしたとき タッチしたときは、新しい折れ線のオブジェクトを生成して、そこをスタート地点にする。 case MotionEvent.ACTION_DOWN: // (7) path = new Path(); path.moveTo(event.getX(), event.getY()); break; (8)(9) 指が離れたとき 指が離れたときは、その座標を折れ線の最後にする。 また、タッチして、その座標で指を離せば、そこにサークルを描く。 case MotionEvent.ACTION_UP: // (8) if (prevAction == MotionEvent.ACTION_DOWN) { // (9) dots.add(new PointWithPaint(event.getX(), event.getY(), paint)); } lines.add(new PathWithPaint(path, paint)); invalidate(); (10) 指を動かしているとき 指を動かしているときは、その座標を折れ線の点とする。 case MotionEvent.ACTION_MOVE: // (10) path.lineTo(event.getX(), event.getY()); (11) 前回のアクションを保持 prevAction = event.getAction(); // (11) (12) アクションは処理済とする return true; // (12) この返り値が false のままだと、未処理となるので、つぎのイベントを拾えない。 今回は、MotionEvent を拾うハンドラーは、このメソッドなので、true にしておく。 ■ データ 描画するデータを保持するクラス。 今回は、色と線の太さも保持しないといけないので、 タッチされた座標と Paint オブジェクトのペアを持つクラス PointWithPaint と、 移動した座標群と Paint オブジェクトのペアを持つクラス PathWithPaint の 2 つのクラスを作る。 □ PointWithPaint.java --- package jp.marunomaruno.android.paintpot; import android.graphics.Paint; import android.graphics.PointF; /** * AppInventor のサンプル PaintPot の Java 版。 * Paintオブジェクトつきの点 * @author marunomaruno * @version 1.0, 2011-12-25 */ public class PointWithPaint { float x; float y; Paint paint; public PointWithPaint(float x, float y, Paint paint) { this.x = x; this.y = y; this.paint = new Paint(paint); // (1) this.paint.setStyle(Paint.Style.FILL); // 塗りつぶす // (2) } public PointWithPaint(PointF point, Paint paint) { this(point.x, point.y, paint); } @Override public String toString() { return String.format("[%.1f, %.1f, %.1f, %x]", x, y, paint .getStrokeWidth(), paint.getColor()); } } --- (1)(2) あたらしいペイント・オブジェクトを生成 あたらしいペイント・オブジェクトとして保持する必要がある。そうでないと、最後に設 定したペイント・オブジェクトの値が描画時に採用されることになる。 this.paint = new Paint(paint); // (1) また、円を描くので、ここでは、塗りつぶしのスタイルにする。 this.paint.setStyle(Paint.Style.FILL); // 塗りつぶす // (2) □PathWithPaint.java --- package jp.marunomaruno.android.paintpot; import android.graphics.Paint; import android.graphics.Path; /** * AppInventor のサンプル PaintPot の Java 版。 * Paintオブジェクトつきの折れ線。 * @author marunomaruno * @version 1.0, 2011-12-25 */ public class PathWithPaint { Path path; Paint paint; public PathWithPaint(Path path, Paint paint) { super(); this.path = path; this.paint = new Paint(paint); // (1) this.paint.setStyle(Paint.Style.STROKE); // 輪郭のみ // (2) } @Override public String toString() { return String.format("[%s, %.1f, %x]", getPathString(), paint .getStrokeWidth(), paint.getColor()); } private String getPathString() { return path.toString(); } } --- (1)(2) あたらしいペイント・オブジェクトを生成 あたらしいペイント・オブジェクトとして保持する必要がある。そうでないと、最後に設 定したペイント・オブジェクトの値が描画時に採用されることになる。 this.paint = new Paint(paint); // (1) また、折れ線を描くので、ここでは、輪郭のみのスタイルにする。 this.paint.setStyle(Paint.Style.STROKE); // 輪郭のみ // (2) ■ レイアウト □ 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:id="@+id/linearLayout1" android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/redButton" android:onClick="onRedButtonClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/redButton" /> <Button android:id="@+id/greenButton" android:onClick="onGreenButtonClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/greenButton" /> <Button android:id="@+id/blueButton" android:onClick="onBlueButtonClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/blueButton" /> </LinearLayout> <jp.marunomaruno.android.paintpot.CanvasView android:id="@+id/canvasView" android:layout_width="fill_parent" android:layout_height="300px" android:scaleType="fitXY" android:src="@drawable/kitty" /> <!-- (1) --> <LinearLayout android:id="@+id/linearLayout2" android:layout_width="match_parent" android:layout_height="wrap_content" > <Button android:id="@+id/wipeButton" android:onClick="onWipeButtonClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/wipeButton" /> <Button android:id="@+id/heavyButton" android:onClick="onHeavyButtonClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/heavyButton" /> <Button android:id="@+id/thinButton" android:onClick="onThinButtonClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/thinButton" /> </LinearLayout> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Medium Text" android:textAppearance="?android:attr/textAppearanceMedium" /> </LinearLayout> --- (1) カスタムビュー 要素名に、カスタム・ビューの完全修飾名をしていするだけでよい。 <jp.marunomaruno.android.paintpot.CanvasView android:id="@+id/canvasView" android:layout_width="fill_parent" android:layout_height="300px" android:scaleType="fitXY" android:src="@drawable/kitty" /> <!-- (1) --> ここで、指定された大きさ (layout_width と layout_height) に対して、画像を自動的 にその大きさに拡大・縮小して表示する属性を指定しておきます。 android:scaleType="fitXY" 値は以下のものが指定できる。 ------------- ----------------------- -------- ---------- 値 説明 リサイズ 縦横比維持 ------------- ----------------------- -------- ---------- matrix 描画時に Matrix を使用 O X fitXY 表示領域の大きさにする O X fitStart 左上端寄せ O O fitCenter 中央寄せ O O fitEnd 右下端寄せ O O center 中央寄せ(リサイズなし) X - centerCrop 中央寄せ(余白含める) O O centerInside 中央寄せ(余白含めない) O O ------------- ----------------------- -------- ---------- ■ 文字列 □ res/values/strings.xml --- <?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, PaintPotActivity!</string> <string name="app_name">PaintPot</string> <string name="redButton">赤</string> <string name="greenButton">緑</string> <string name="blueButton">青</string> <string name="wipeButton">クリア</string> <string name="heavyButton">太線</string> <string name="thinButton">細線</string> </resources> ---
MIT 版 App Inventor のインストール ================================================================================ 参考を基にして、MIT 版 App Inventor をインストールした。 なお、文中の 【AppId】は、自分の登録したアプリケーションID 【email】は、アプリケーションID を登録した email を示す。 □ 参考 「Running App Inventor services with the MIT JAR files」の翻訳 http://www.app-inventor.jp/shiryoushuu/transrate MIT 版App Inventorをローカルで動かしてみた。動いたよ! - gabuchanの日記 http://d.hatena.ne.jp/gabuchan/20111217/1324133785 □ 環境の前提 OS: CentOS 5.5 JDK 1.6 また、作業は root で行った ■ 1. Google App Engine の設定 1. Google App Engine のダウンロード つぎのURLからダウンロードする Java 用 Google App Engine SDK http://code.google.com/intl/ja/appengine/downloads.html#Google_App_Engine_SDK_for_Java appengine-java-sdk-1.6.1.zip 2. このファイルを展開して、/usr/local/ に配置 3. この中のコマンドを使えるように、パスに設定 ログインプロファイルの更新と反映 編集する # vi .bash_profile つぎを追加する PATH=$PATH:/usr/local/appengine-java-sdk-1.6.1/bin 設定値をプロンプトに反映させる # source .bash_profile ■ 2. MIT 版 App Inventor の設定 1. MIT 版 App Inventor サービス ZIP 版のダウンロード つぎのURLからダウンロードする Download .zip File to Run Your Own Instance of App Inventor http://appinventoredu.mit.edu/download-jar-files appinventor-service-Dec-20.zip ※2011-12-22 現在、上記のファイルが最新 キュメント https://docs.google.com/document/d/124V0q-Jzs8n9LqAlFKnSWxGLei_KZAUQGJUZwlALVws/ edit?hl=en_US&pli=1 2. ファイルを解凍 つぎの 2 つの tgz ファイルが出る。 appinventor-service-Dec-20.zip_FILES + appinventor-service-Dec-20 + appinventor-Nov-24.tgz App Engine 用 + for-BuildServer.tgz ビルドサーバー 用 なお、__MACOSX というディレクトリーもできるが無視。 ■ 3. ビルドサーバーの構築 1. App Engine 用ファイルの展開 appinventor-Nov-24.tgz : App Engine用 解凍後 appinventor-service-Dec-20.zip_FILES + appinventor-service-Dec-20 + appinventor-Dec-20 + appinventor + war + WEB-INF | + appengine-web.xml App Engine の記述子 + whitelist ホワイトリスト 2. ビルドサーバーの指定 appinventor-Nov-24.tgz を展開して、そこの中にある appinventor/war/WEB-INF/appengine-web.xml ファイルを編集する。 ビルドサーバーは、values 属性値で指定する。なお、ポート番号は変更できない。 <property name="build.server.host" value="localhost:9990" /> ※ローカルホストで行うのでこのまま ホワイトリストを使うのであれば、value="true" にする。 <property name="use.whitelist" value="false"/> このとき、つぎのファイルの中のリストを設定する。 war/whitelist ※ホワイトリストを使わないのでこのまま 3. for-BuildServer.tgz を展開 このファイルはビルドサーバーに持っていく。 ※今回は、ローカルなのでこのままにしておく。 for-BuildServer.tgz : ビルドサーバ用 + for-BuildServer + lib + launch-buildserver 4. ビルドサーバーを起動 つぎのコマンドを実行する。 --- # killall java # ./launch-buildserver --- 次のコマンドでログを確認できる。 --- # cat -f buildserver-log.out class com.google.appinventor.buildserver.BuildServer 2011/12/22 10:11:01 com.sun.jersey.api.core.ScanningResourceConfig init 情報: No provider classes found. 2011/12/22 10:11:01 com.sun.jersey.server.impl.application.WebApplicationImpl _i nitiate 情報: Initiating Jersey application, version 'Jersey: 1.3 06/17/2010 05:04 PM' 2011/12/22 10:11:02 com.sun.grizzly.Controller logVersion 情報: Starting Grizzly Framework 1.9.18-i - Thu Dec 22 10:11:02 JST 2011 App Inventor Build Server - Version: 42 Id: 40fd4340860b Visit: http://127.0.0.1:9990/buildserver Server running --- なお、 http://127.0.0.1:9990/buildserver を見ても、何も(エラーも)表示されない。 ※終了させるコマンドが見当たらないようなので、kill コマンドで終了させる。 ■ 4. App Engine の構築 1. App Engine に App Inventor サービスをアップロード つぎのコマンドでアップロードする --- # appcfg.sh -A 【AppId】 update war Reading application configuration data... 2011/12/22 10:37:04 com.google.apphosting.utils.config.AppEngineWebXmlReader rea dAppEngineWebXml 情報: Successfully processed war/WEB-INF/appengine-web.xml 2011/12/22 10:37:04 com.google.apphosting.utils.config.AbstractConfigXmlReader r eadConfigXml 情報: Successfully processed war/WEB-INF/web.xml 2011/12/22 10:37:04 com.google.apphosting.utils.config.IndexesXmlReader readConf igXml 情報: Successfully processed war/WEB-INF/appengine-generated/datastore-indexes-a uto.xml Beginning server interaction for 【AppId】... Email: 【email】 Password for 【email】: 0% Created staging directory at: '/tmp/appcfg5728424899164162038.tmp' 5% Scanning for jsp files. 20% Scanning files on local disk. 25% Scanned 250 files. : (中略) 99% Will check again in 4 seconds. 99% Closing update: new version is ready to start serving. 99% Uploading index definitions. Update completed successfully. Success. Cleaning up temporary files... --- ■ 5. MIT 版 App Inventor を使う 1. ブラウザーでアクセス Webブラウザーで、つぎのアドレスで、App Inventor が使える http://【AppId】.appspot.com/ 2. ログイン 最初にログイン・ダイアログが表示されるが、今回はホワイトリストを設定していないの で、そのまま「Log In」をクリックする。 3. 利用規約に同意 つぎのメッセージが表示されるので、「Terms of Service」をクリックして同意 --- To use App Inventor for Android, you must accept the following terms of service. Terms of Service You agree to have lots of fun learning to use App Inventor and making apps for y our Android device! --- 4. App Inventor を使う つぎのメッセージが赤字で表示 --- This is an experimental version of App Inventor. IT IS FOR TESTING ONLY, NOT FOR GENERAL USE! --- あとは、普通に使えると思う。 ソースのアップロードも普通に行えた。 実機でも動作が確認できた。 ■ 6. ローカル環境で使う 1. [3. ビルドサーバーの構築] まで行ったら、つぎのコマンドでApp Engine を構築する。 --- # dev_appserver.sh war/ 2011/12/22 1:40:47 com.google.apphosting.utils.jetty.JettyLogger info 情報: Logging to JettyLogger(null) via com.google.apphosting.utils.jetty.JettyLo gger : (中略) 2011/12/22 1:40:49 com.google.appengine.tools.development.DevAppServerImpl start 情報: The server is running at http://localhost:8080/ 2011/12/22 1:40:49 com.google.appengine.tools.development.DevAppServerImpl start 情報: The admin console is running at http://localhost:8080/_ah/admin --- 2. 使う ローカルの場合、赤い色で次が出る --- This is an experimental version of App Inventor. IT IS FOR TESTING ONLY, NOT FOR GENERAL USE! Note: This App Inventor instance is not being hosted on AppEngine. As a result, it will not correctly save your projects when you log out. You'll have to download them if you want them saved. --- ローカルなので、プロジェクトの保存が必要、とのこと。 ★ローカルで実行したときに、なんと、ブロックエディッターで、日本語表示ができた。 うれしい。 ただ、なぜできたかは今のところ不明。 ※こちらも、kill コマンドで終了かな。
標準出力 - あて先に画面を追加する ================================================================================ ■ 標準出力のあて先に画面を追加する 標準出力(標準エラー出力も含む)のあて先は、LogCat。 たとえば、 System.out.print("test"); としたときは、つぎと同じことになる。 Log.i("System.out", "test"); このあて先に、画面(TextView)も追加する。 これには、PrintStream クラスを継承したクラスを作って、System.setOut() メソッドで、 標準出力オブジェクトを置き換えてやればよい。 画面でスクロールさせるために、res/layout/main.xml に ScrollView を設定してあげる 必要がある。 ◆ アクティビティのクラス。 □ SystemOut01Activity.java --- package jp.marunomaruno.android.sample; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; /** * @author marunomaruno * @version 1.0, 2011-12-14 * @since 1.0 */ public class SystemOut01Activity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // 標準出力を置き換え(標準出力(LogCat)+テキストビュー) TextView sysout = (TextView) findViewById(R.id.sysout); System.setOut(new TextViewPrintStream(System.out, sysout)); // (1) System.setErr(new TextViewPrintStream(System.err, sysout)); // (2) // 出力する for (int i = -50; i < 50; i++) { System.out.printf("%3d|", i); // (3) for (int j = 0; j < Math.abs(i); j++) { System.out.print("*"); } System.out.println(); } // 例外をスローする try { int i = 1 / 0; } catch (Exception e) { e.printStackTrace(); // (4) } } } --- (1)(2) 標準出力、標準エラー出力の置き換え System.setOut(new TextViewPrintStream(System.out, sysout)); // (1) System.setErr(new TextViewPrintStream(System.err, sysout)); // (2) こうすることで、標準出力、標準エラー出力のどちらも指定した TextView で出力させる ことができる。 (3) 標準出力を使う System.out.printf("%3d|", i); // (3) (4) 標準エラー出力を使う e.printStackTrace(); // (4) ◆ 標準出力をテキストビューにも出力させるクラス。 PrintStream クラスを継承したクラスを作って、print(String) メソッドをオーバーライ ドすればよい。 □ TextViewPrintStream.java --- package jp.marunomaruno.android.sample; import java.io.File; import java.io.FileNotFoundException; import java.io.OutputStream; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import android.view.View; import android.widget.ScrollView; import android.widget.TextView; /** * 標準入出力を Log と テキストビューに出力させるようにするクラス. * 指定されたテキストビューは、スクロールビューに囲まれている必要がある。 * @author marunomaruno * @version 1.0, 2011-12-14 * @since 1.0 */ public class TextViewPrintStream extends PrintStream { // (1) private TextView view; // (2) private Runnable scrollDown = new ScrollDown(); // (3) public TextViewPrintStream(File file, String csn, TextView view) throws FileNotFoundException, UnsupportedEncodingException { // (4) super(file, csn); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } public TextViewPrintStream(File file, TextView view) throws FileNotFoundException { // (4) super(file); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } public TextViewPrintStream(OutputStream out, boolean autoFlush, String enc, TextView view) throws UnsupportedEncodingException { // (4) super(out, autoFlush, enc); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } public TextViewPrintStream(OutputStream out, boolean autoFlush, TextView view) { // (4) super(out, autoFlush); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } public TextViewPrintStream(OutputStream out, TextView view) { // (4) super(out); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } public TextViewPrintStream(String fileName, String csn, TextView view) throws FileNotFoundException, UnsupportedEncodingException { // (4) super(fileName, csn); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } public TextViewPrintStream(String fileName, TextView view) throws FileNotFoundException { // (4) super(fileName); this.view = view; ((View) view.getParent()).post(scrollDown); // (5) } @Override public synchronized void print(String str) { // (6) super.print(str); view.append(str); // (7) } /** * スクロールダウンさせるスレッドのクラス * @author marunomaruno */ private class ScrollDown implements Runnable { // (8) public void run() { ((ScrollView) view.getParent()).fullScroll(View.FOCUS_DOWN); // (9) } } } --- (1) PrintStream クラスのサブクラスを作る public class TextViewPrintStream extends PrintStream { // (1) (2) 出力先のオブジェクト private TextView view; // (2) テキストビューに出力させるので、それを保持しておく。 (3) スクロールダウンさせるスレッドのオブジェクト private Runnable scrollDown = new ScrollDown(); // (3) ScrollDown クラスは、(8)で作成するプライベートな内部クラス。 (4) コンストラクター 現行の PrintStream クラスのコンストラクターの引数の後ろに、出力先になる TextView を指定しておく。 public TextViewPrintStream(File file, String csn, TextView view) throws FileNotFoundException, UnsupportedEncodingException { // (4) public TextViewPrintStream(File file, TextView view) throws FileNotFoundException { // (4) public TextViewPrintStream(OutputStream out, boolean autoFlush, String enc, TextView view) throws UnsupportedEncodingException { // (4) public TextViewPrintStream(OutputStream out, boolean autoFlush, TextView view) { // (4) public TextViewPrintStream(OutputStream out, TextView view) { // (4) public TextViewPrintStream(String fileName, String csn, TextView view) throws FileNotFoundException, UnsupportedEncodingException { // (4) public TextViewPrintStream(String fileName, TextView view) throws FileNotFoundException { // (4) (5) このテキストビューを囲むスクロールビュー・オブジェクトに対してつねに最下段を 指すように指定 ((View) view.getParent()).post(scrollDown); // (5) (6) print メソッドのオーバーライド public synchronized void print(String str) { // (6) このメソッドをオーバーライドすることで、自分の意図した PrintStream オブジェクト になる。 (7) TextView に文字列を追加する view.append(str); // (7) (8) スクロールさせるためのクラス private class ScrollDown implements Runnable { // (8) (9) スクロールさせる このテキストビューを囲むスクロールビュー・オブジェクトに対して、最下段を表示する ように指定する。 ((ScrollView) view.getParent()).fullScroll(View.FOCUS_DOWN); // (9) □View クラスのどの位置にするかの定数 --- int FOCUS_BACKWARD int FOCUS_DOWN int FOCUS_FORWARD int FOCUS_LEFT int FOCUS_RIGHT int FOCUS_UP --- □ScrollView.fullScroll メソッド --- public boolean fullScroll (int direction) --- direction に、上記の定数のうち、FOCUS_DOWN または FOCUS_UP を指定する。 ◆ レイアウト □ 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" > <ScrollView android:id="@+id/sysoutScrollView" android:layout_width="fill_parent" android:layout_height="wrap_content" > <!-- (1) --> <TextView android:id="@+id/sysout" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <!-- (2) --> </ScrollView> </LinearLayout> --- (1) スクロールビュー (2) 標準出力のあて先のテキストビュー ◆ 文字列の定数 □ res/values/strings.xml --- <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">SystemOut01</string> </resources> ---
位置情報 (1) 無線ネットワークを使った位置情報 ================================================================================ ■ 無線ネットワークを使った位置情報 無線ネットワークを使った位置情報を取得する。 これも、センサーを使ったものと同じようにプログラミングできる。 基本は、以下の手順による。 1. LocationManager のオブジェクトを取得する 2. LocationManager オブジェクトに位置情報取得のリスナーを登録する 3. 位置情報が変化したら、その値を取得する ◆ アクティビティのクラス。 □ Gps01Activity.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.widget.TextView; /** * 位置情報を取得して表示する。 * @author marunomaruno * @version 1.0, 2011-12-14 * @since 1.0 */ public class Gps01Activity extends Activity { // TextView private TextView sensorValueTextView; // GPS値 private LocationManager locationManager; // (1) private LocationListener sensorEventListener; // onPause()が呼ばれたときに、すべてのリスナーを解除するため @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); sensorValueTextView = (TextView) findViewById(R.id.sensor_value); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); // (2) Log.d(getString(R.string.logTag), "locationManager = " + locationManager); } @Override protected void onResume() { super.onResume(); // プロバイダーを取得する条件を作成する Criteria fineAndLowPower = new Criteria(); // (3) fineAndLowPower.setAccuracy(Criteria.ACCURACY_FINE); // (4) fineAndLowPower.setPowerRequirement(Criteria.POWER_LOW); // (5) fineAndLowPower.setAltitudeRequired(true); // (6) fineAndLowPower.setBearingRequired(true); // (7) fineAndLowPower.setSpeedRequired(true); // (8) // プロバイダーを取得する String provider = locationManager .getBestProvider(fineAndLowPower, true); // (9) Log.d(getString(R.string.logTag), "provider = " + provider); TextView providerNameTextView = (TextView) findViewById(R.id.provider_name); providerNameTextView.setText(provider); // プロバイダーがみつからなければ、NETWORK_PROVIDER を設定しておく if (provider == null) { // (10) provider = LocationManager.NETWORK_PROVIDER; providerNameTextView.setText(R.string.no_provider); } // 位置情報のリスナーを取得して登録する sensorEventListener = new GpsLocationListener(sensorValueTextView); // (11) locationManager.requestLocationUpdates( provider, 0, 0, sensorEventListener); // (12) } @Override public void onPause() { super.onPause(); // すべてのリスナーを解除する locationManager.removeUpdates(sensorEventListener); // (13) } } --- (1)(2) LocationManager とその取得 private LocationManager locationManager; // (1) □ LocationManager LocationManager は、位置情報を管理するクラス。ここに、位置情報が変更すると起こる イベントに対するリスナーを登録する。 java.lang.Object + android.location.LocationManager このクラスのオブジェクトは、 Context.getSystemService(Context.LOCATION_SERVICE) をとおして取得する。実際は(2)を参照。 Context のサブクラスである Activity クラスのサブクラス中なので、つぎのように取得 できる。 locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); // (2) (3)-(8) プロバイダーを取得する基準を作成する 位置情報を取得するためのプロバイダーを選ぶための基準を作成する。この基準に基づい て、システムは最適と思われるプロバイダーを選ぶ。 Criteria fineAndLowPower = new Criteria(); // (3) 基準オブジェクトを作成して、つぎのメソッドを使って基準を設定する。 fineAndLowPower.setAccuracy(Criteria.ACCURACY_FINE); // (4) fineAndLowPower.setPowerRequirement(Criteria.POWER_LOW); // (5) fineAndLowPower.setAltitudeRequired(true); // (6) fineAndLowPower.setBearingRequired(true); // (7) fineAndLowPower.setSpeedRequired(true); // (8) □ Criteria クラス プロバイダーを選択する上での基準になる。ここに、選択する基準を設定する。 java.lang.Object + android.location.Criteria □ 定数 --- int ACCURACY_COARSE おおよその精度 int ACCURACY_FINE 細かい精度 int ACCURACY_HIGH 高い精度 int ACCURACY_LOW 低い精度 int ACCURACY_MEDIUM 中間の精度 int NO_REQUIREMENT 要求しない int POWER_HIGH 電池を多く使う int POWER_LOW 電池をそれほど使わない int POWER_MEDIUM 電池を普通に使う --- □ コンストラクター --- Criteria() Criteria(Criteria criteria) --- □ おもな基準を設定するメソッド --- void setAccuracy(int accuracy) 緯度・経度の精度を設定 void setBearingAccuracy(int accuracy) ベアリングの精度を設定 void setHorizontalAccuracy(int accuracy) 緯度・経度の精度を設定 void setPowerRequirement(int level) 最大電力レベルを設定 void setSpeedAccuracy(int accuracy) 速度の精度を設定 void setVerticalAccuracy(int accuracy) 高度の精度を設定 void setCostAllowed(boolean costAllowed) プロバイダがコストを負担するかどうか void setAltitudeRequired(boolean altitudeRequired) 標高を出すかどうか void setBearingRequired(boolean bearingRequired) ベアリングを出すかどうか void setSpeedRequired(boolean speedRequired) 速度を出すかどうか --- なお、それぞれの基準を設定するメソッドによって、指定できる基準の定数が決まってい る。この定数以外だと、IllegalArgumentException 例外がスロー。 --------------------- ------ ------ ------ ------ ------ -------------- COARSE FINE HIGH LOW MEDIUM NO_REQUIREMENT --------------------- ------ ------ ------ ------ ------ -------------- setAccuracy ○ ○ setBearingAccuracy ○ ○ ○ setHorizontalAccuracy ○ ○ ○ ○ setSpeedAccuracy ○ ○ ○ setVerticalAccuracy ○ ○ ○ ○ --------------------- ------ ------ ------ ------ ------ -------------- ★ 今回のサンプルの(6)-(8)のように、標高、ベアリング、速度を要求しても、ネット ワーク・プロバイダーの場合、できなければ 0 が返るようだ。 GPS プロバイダーの場合、動きが止まるようだ(?)。 (9)(10) プロバイダーを取得する 作成した基準に基づいて、プロバイダーを取得する。 String provider = locationManager .getBestProvider(fineAndLowPower, true); // (9) if (provider == null) { // (10) provider = LocationManager.NETWORK_PROVIDER; providerNameTextView.setText(R.string.no_provider); } プロバイダーを取得するのは、基準を指定して、その基準にしたがって、よりよいプロバ イダーをシステムが選択する。 getBestProvider() メソッドの 2 番目の引数 enabledOnly が true の場合は、使用でき るものだけが取得されるの。使用できるプロバイダーがなければ、null が返る。 □ メソッド --- String getBestProvider(Criteria criteria, boolean enabledOnly) 基準を満たすプロバイダー名を取得する --- 上記以外にも、プロバイダーを取得したり、確認するメソッドがある。 --- List<String> getAllProviders() すべてのプロバイダー名を取得する LocationProvider getProvider(String name) 指定されたプロバイダー名のプロバイダーのオブジェクトを取得する List<String> getProviders(boolean enabledOnly) 基準を満たす、すべてのプロバイダー名を取得する List<String> getProviders(Criteria criteria, boolean enabledOnly) 基準を満たす、すべてのプロバイダー名を取得する boolean isProviderEnabled(String provider) 指定されたプロバイダー名のプロバイダーが使用できるかどうかを返す --- (11)(12) 位置情報のリスナーを取得して登録 センサーのときと同じように、リスナーのオブジェクトを取得する。 sensorEventListener = new GpsLocationListener(sensorValueTextView); // (11) 位置情報のリスナーは、LocationListener インターフェースを実装しているオブジェク トで、つぎのメソッドを実装しなければならない。 abstract void onLocationChanged(Location location) abstract void onProviderDisabled(String provider) abstract void onProviderEnabled(String provider) abstract void onStatusChanged(String provider, int status, Bundle extras) 位置情報リスナーの登録には、LocationManager.requestLocationUpdates() メソッドを 使う。 locationManager.requestLocationUpdates( provider, 0, 0, sensorEventListener); // (12) このメソッドは、いくつかオーバーロードされている。 □ requestLocationUpdates のオーバーロードされたメソッド --- void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, PendingIntent intent) 基準 に基づいた インテント を登録する void requestLocationUpdates(long minTime, float minDistance, Criteria criteria, LocationListener listener, Looper looper) 基準 に基づいた リスナー を登録する void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener) プロバイダーに基づいた リスナー を登録する void requestLocationUpdates(String provider, long minTime, float minDistance, LocationListener listener, Looper looper) プロバイダーに基づいた リスナー を登録する void requestLocationUpdates(String provider, long minTime, float minDistance, PendingIntent intent) プロバイダーに基づいた インテント を登録する --- それぞれの引数 --- long minTime 位置情報を更新する最小の時間間隔(ミリ秒) float minDistance 位置情報を更新する最小の距離間隔(メートル) Criteria criteria 基準 PendingIntent intent 位置を更新するためのインテント LocationListener listener 位置が更新されたときに呼ばれるリスナー Looper looper メッセージループのオブジェクト String provider プロバイダー --- なお、この引数 minTime や minDistance は、指定したとおりになるかどうかはわからな い。プロバイダー次第か。 □ プロバイダー LocationManager クラスのプロバイダー関係の定数 --- String GPS_PROVIDER GPS ロケーションを利用したプロバイダー String NETWORK_PROVIDER ネットワークを利用したプロバイダー String PASSIVE_PROVIDER 位置情報の修正を行わない特殊なプロバイダー --- (13) すべてのリスナーを解除する locationManager.removeUpdates(sensorEventListener); // (13) ◆ 位置情報のリスナーのクラス。 位置情報が変わったことに対するリスナーのクラス。LocationListener インターフェー スを実装する。 位置情報が変わったら、onLocationChanged() が呼ばれる。 □ GpsLocationListener.java --- package jp.marunomaruno.android.sample; import android.location.Location; import android.location.LocationListener; import android.location.LocationProvider; import android.os.Bundle; import android.util.Log; import android.widget.TextView; /** * GPSによるロケーションのリスナー * * @author maruno * @version 1.0, 2011-12-14 * @since 1.0 */ public class GpsLocationListener implements LocationListener { // (1) protected TextView textView; public GpsLocationListener(TextView textView) { this.textView = textView; } @Override public void onLocationChanged(Location location) { // (2) Log.d(textView.getResources().getString(R.string.logTag), "onLocationChanged()"); textView.setText(""); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_timestamp), textView.getResources().getString(R.string.time), location.getTime())); // (3) textView.append("\n"); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_real), textView.getResources().getString(R.string.latitude), location.getLatitude())); // (4) textView.append("\n"); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_real), textView.getResources().getString(R.string.longitude), location.getLongitude())); // (5) textView.append("\n"); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_real), textView.getResources().getString(R.string.accuracy), location.getAccuracy())); // (6) textView.append("\n"); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_real), textView.getResources().getString(R.string.altitude), location.getAltitude())); // (7) textView.append("\n"); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_real), textView.getResources().getString(R.string.speed), location.getSpeed())); // (8) textView.append("\n"); textView.append(String.format(textView.getResources().getString( R.string.sensor_value_format_for_real), textView.getResources().getString(R.string.bearing), location.getBearing())); // (9) textView.append("\n"); } @Override public void onProviderDisabled(String provider) { // (10) Log.d(textView.getResources().getString(R.string.logTag), "onProviderDisabled()"); } @Override public void onProviderEnabled(String provider) { // (11) Log.d(textView.getResources().getString(R.string.logTag), "onProviderEnabled()"); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { // (12) Log.d("TEST", "onStatusChanged()"); switch (status) { case LocationProvider.AVAILABLE: // (13) Log.d(textView.getResources().getString(R.string.logTag), "onStatusChanged() - AVAILABLE"); break; case LocationProvider.OUT_OF_SERVICE: // (14) Log.d(textView.getResources().getString(R.string.logTag), "onStatusChanged() - OUT_OF_SERVICE"); break; case LocationProvider.TEMPORARILY_UNAVAILABLE: // (15) Log.d(textView.getResources().getString(R.string.logTag), "onStatusChanged() - TEMPORARILY_UNAVAILABLE"); break; default: Log.d(textView.getResources().getString(R.string.logTag), "onStatusChanged() - Other"); break; } } } --- (1) LocationListener インターフェースを実装 位置情報を取得するリスナーは、LocationListener インターフェースを実装する。 public class GpsLocationListener implements LocationListener { // (1) □ LocationListener インターフェース 実装すべきメソッド --- abstract void onLocationChanged(Location location) 場所が変更されたときに呼び出される abstract void onProviderDisabled(String provider) プロバイダーが無効になったときに呼び出される abstract void onProviderEnabled(String provider) プロバイダーが有効になったときに呼び出される abstract void onStatusChanged(String provider, int status, Bundle extras) プロバイダーの状態が変更されたときに呼び出される --- (2) 場所が変更されたときに呼び出されるハンドラー・メソッド つぎのメソッドを実装する。 public void onLocationChanged(Location location) { // (2) 引数は Location オブジェクト。 □ Location クラス 位置情報を持つクラス。 java.lang.Object + android.location.Location 定数は、以下の 3 つがある。これは、緯度や経度の形式を決める。 定数 --- int FORMAT_DEGREES [+-]DDD.DDDDD、 D: 度 int FORMAT_MINUTES [+-]DDD:MM.MMMMM、D: 度、M: 分、1 度 = 60 分 int FORMAT_SECONDS [+-]DDD:MM:SS.SSSSS、D: 度、M: 分、S: 秒、1 度 = 60 分、1 分 = 60 秒 --- おもなメソッド --- static String convert (double coordinate, int outputType) 度(coordinate)を指定された緯度や経度の形式(outputType)の文字列にする float getAccuracy() 精度 double getAltitude() 標高 float getBearing() ベアリング Bundle getExtras() プロバイダー固有の情報 double getLatitude() 緯度 double getLongitude() 経度 String getProvider() プロバイダー float getSpeed() 速度 long getTime() 時間 --- convert() メソッドの実行例 Location.convert(123.456789, Location.FORMAT_DEGREES) = "123.45679" Location.convert(123.456789, Location.FORMAT_MINUTES) = "123:27.40734" Location.convert(123.456789, Location.FORMAT_SECONDS) = "123.27:24.4404" (3)-(9) 次のメソッドで、緯度や経度を取得している。 location.getTime())); // (3) location.getLatitude())); // (4) location.getLongitude())); // (5) location.getAccuracy())); // (6) location.getAltitude())); // (7) location.getSpeed())); // (8) location.getBearing())); // (9) (10) プロバイダーが無効になったときに呼び出されるハンドラー・メソッド public void onProviderDisabled(String provider) { // (10) (11) プロバイダーが有効になったときに呼び出されるハンドラー・メソッド public void onProviderEnabled(String provider) { // (11) (12) プロバイダーの状態が変更されたときに呼び出されるハンドラー・メソッド public void onStatusChanged(String provider, int status, Bundle extras) { // (12) 今回のように、ひとつのプロバイダーを登録しているだけだと動かないようだ。 (13)-(15) 状態の変化 LocationProvider の定数によって、どいう状態かを判断する。 case LocationProvider.AVAILABLE: // (13) case LocationProvider.OUT_OF_SERVICE: // (14) case LocationProvider.TEMPORARILY_UNAVAILABLE: // (15) LocationProvider の定数 --- int AVAILABLE int OUT_OF_SERVICE int TEMPORARILY_UNAVAILABLE --- ◆ レイアウト □ 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" > <!-- プロバイダー名 --> <TextView android:id="@+id/provider_name" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <!-- センサー値 --> <TextView android:id="@+id/sensor_value" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> --- ◆ 文字列の定数 □ res/values/string.xml --- <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Gps01</string> <string name="logTag">TEST</string> <string name="no_provider">プロバイダーは見つかりませんでした。</string> <string name="gpsUnit">m/s^2</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> </resources> --- ◆マニフェスト ユーザーパーミッションをつける。 □AndroidManifest.xml --- <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.marunomaruno.android.sample" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!-- (1) --> <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/> <!-- (2) --> <uses-permission android:name="android.permission.INTERNET"/> <!-- (3) --> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:label="@string/app_name" android:name=".Gps01Activity" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> --- (1)-(3) パーミッションは以下のものを使う ACCESS_FINE_LOCATION GPS を使う ACCESS_MOCK_LOCATION エミュレーターで GPS をエミュレートする INTERNET 無線を使う これを、AndroidManifest.xml に記述する。