トップ » 技術記事 » Android で再開する Java プログラミング(9) - Android ライフサイクルを考慮-手書きメモを作り込む

Android で再開する Java プログラミング(9) - Android ライフサイクルを考慮-手書きメモを作り込む

タグ: Android Java

前回、手書きメモを作ってみました。そこそこ完成したのですが、問題がありました。画面の向きを変えたり、誤って[戻る]ボタンを押した時に、描画していたものが消えてしまったのです。そこで、前回の状態を記憶する機能を加えて、手書きメモを完成させてみます。

本体を横にしても画面を回転させない方法は?

HT-03A などは、Android の本体を横にすると、画面モードが縦から横モードへと状態が変化し、onSizeChanged() イベントが発生するようになっています。

手書きメモでは、画面を横にしたとしても、現在表示中のコンテンツを回転させる必要はありません。(横にしたまま、書いてもらっても何の支障もない上に、画面を回転させることで、スクロールが必要だったり、画面を縮小する必要が出てしまいます。)

その場合、Eclipse の Package Explorer から、AndroidManifest.xml を編集し、画面モードが変わらないように設定を変更できます。XML の manifest/application/activity の属性、android:screenOrientation を、"portrait" に設定すると、縦画面のまま回転しなくなります。

  • "portrait" … 縦画面で固定
  • "landscape" … 横画面で固定
  • "unspecified" … デバイス任せ

→詳しいSDKのリファレンス

最後の描画状態を保存する ~ Activity のライフサイクルを知る

次に、何かしらの割り込みがあった場合でも、最後の描画状態を保存するように改良してみます。そのためには、割り込み発生前に呼び出されるイベントで、保存処理を行うようにする必要があります。

Android のアプリケーションでは、次のイベントが発生します。

  • onCreate() .. 初めて作成されるとき
  • onStart() .. ユーザーから見えるようになる直前
  • onRestart() .. 停止した後、それをもう一度開始する直前
  • onResume() .. ユーザーからの入力が行われるようになったとき
  • onPause() .. 別のアクティビティを開始しようとしているとき
  • onStop() .. ユーザーから見えなくなったとき
  • onDestroy() .. 破棄される前

これらは、Activity の状態が遷移したときに起きますが、どのように状態が遷移するかについて、SDKのこちらのページの真ん中ほどに、状態の遷移図がありますので、参考になります。

データの永続化を行うのは、onPause() のタイミングで行います。このほかに、onSaveInstanceState() もあり、これは、データの保存が必要なタイミングでイベントが起きます。しかし、ユーザーが[戻る]ボタンをクリックした場合には発生しないようです。ユーザーが明示的にアプリケーションを終了したタイミングでは、データを明示的に保存する必要がないとの判断のようです。

ログを取って確認してみよう!

いくら、ライフタイムの説明を読んだとしても、読んだだけでは、なかなか理解できないものです。実際に操作してみて、どのタイミングで何が起きるのか目視してみましょう。そのために、ログを仕込んで、デバッガで確認してみましょう。

ログを出力するには、android.util.Log を利用すると便利です。以下のように、Activity を継承したクラスへ次のようなログ出力を埋め込んでおきます。ログは、『Log.v(タグ,メッセージ)』の書式で指定します。

import android.util.Log;
~~~
    protected void onResume() {
    	super.onResume();
    	Log.v("DrawNoteK", "onResume");
    }
    protected void onRestart () {
    	super.onRestart();
    	Log.v("DrawNoteK", "onRestart");
    }
    protected void onStart (){
    	super.onStart();
    	Log.v("DrawNoteK", "onStart");
    }
    protected void onStop (){
    	super.onStop();
    	Log.v("DrawNoteK", "onStop");
    }
    protected void onPause () {
    	super.onPause();
    	Log.v("DrawNoteK", "onPause");
    }
    protected void onSaveInstanceState(Bundle outState) {
    	super.onSaveInstanceState(outState);
    	Log.v("DrawNoteK", "onSaveInstanceState");
    }
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
    	super.onRestoreInstanceState(savedInstanceState);
    	Log.v("DrawNoteK", "onRestoreInstanceState");
    }
~~~

手書きメモで前回の状態を覚えておくように工夫する

さて、手書きメモの場合は、クリアメニューもあることですし、いつでも、前回の状態へ戻すような配慮をするように作り込んでみたいと思います。

ライフサイクルの表を確認してみると、onStart() と onStop() を実装してみると良さそうな感じがしました。

そこで、このタイミングで、アプリケーションのデータフォルダへ、cache.png という一時ファイルを作ることにしました。そして、以下のように実装してみましたが、うまくいきません。

    protected void onStart (){
    	Log.v("DrawNoteK", "onStart");
    	super.onStart();
    	view.loadFromCacheFile(); // 一時ファイルから画面を復元
    }
    protected void onStop (){
    	Log.v("DrawNoteK", "onStop");
    	super.onStop();
    	view.saveToCacheFile(); // 一時ファイルへ画面を保存
    }

そして、至る所にログを仕込んでみると、画面のサイズを得て描画用の Bitmap を作るイベント view の onSizeChanged() が、onStart() よりも後で呼ばれていることが分かりました。つまり、onStart() で既存の Bitmap に描画しても、onSizeChanged() で Bitmap が作り直されてしまうのです。

そこで、onStart() で一時ファイルを読み込むのではなく、onSizeChanged() で読み込むように変更してみました。すると、良い具合です。

手書きメモ(ver.1.1)

上記の点を踏まえて、前回のプログラムを改良してみました。以下が、その全ソースとなります。縦書き専用として使った方が都合が良いので、AndroidManifest.xml の manifest/application/activity の属性、android:screenOrientation を、"portrait" に設定してから、実行してみて下さい。

package com.kujirahand.android.drawnotek;

import java.io.File;
import java.io.FileOutputStream;
import java.util.Date;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Bitmap.CompressFormat;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;

/** メインクラスの定義 */
public class DrawNoteK extends Activity {
    DrawNoteView view;
    private static final int MENU_CLEAR = 0;
    private static final int MENU_SAVE = 1;
    /** アプリの初期化 */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 描画クラスを設定
        view = new DrawNoteView(getApplication());
        setContentView(view);
    }
    /** メニューの生成イベント */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    	super.onCreateOptionsMenu(menu);
    	menu.add(0, MENU_CLEAR, 0, "Clear");
    	menu.add(0, MENU_SAVE, 0, "Save");
    	return true;
    }
    /** メニューがクリックされた時のイベント */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    	switch ( item.getItemId() ) {
    	case MENU_CLEAR:
    		view.clearDrawList(); break;
    	case MENU_SAVE:
    		view.saveToFile();
    		break;
    	}
    	return true;
    }
    protected void onStop (){
    	super.onStop();
    	Log.v("DrawNoteK", "onStop");
    	view.saveToCacheFile();// 画面を一時ファイルに保存する
    	// 復元は view.onSizeChanged() で行う
    }
}
//--------------------------------------------------------
/** 描画クラスの定義 */
class DrawNoteView extends android.view.View {
	Bitmap bmp = null;
	Canvas bmpCanvas;
	Point oldpos = new Point(-1,-1);
	public DrawNoteView(Context c) {
		super(c);
		setFocusable(true);
	}
	public void clearDrawList() {
		bmpCanvas.drawColor(Color.WHITE);
		invalidate();
	}
	/** 画面サイズが変更された時 */
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		Log.v("DrawNoteK", "view.onSizeChanged");
		super.onSizeChanged(w,h,oldw,oldh);
		if (bmp == null) {
			bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444);
		}
		bmpCanvas = new Canvas(bmp);
		bmpCanvas.drawColor(Color.WHITE);
		//
		this.loadFromCacheFile();
	}
	/** 描画イベント */
	protected void onDraw(Canvas canvas) {
		canvas.drawBitmap(bmp, 0, 0, null);
	}
	/** タッチイベント */
	public boolean onTouchEvent(MotionEvent event) {
		 // 描画位置の確認
		 Point cur = new Point((int)event.getX(), (int)event.getY());
		 if (oldpos.x < 0) { oldpos = cur; }
		 // 描画属性を設定
		 Paint paint = new Paint();
		 paint.setColor(Color.BLUE);
		 paint.setStyle(Paint.Style.FILL);
		 paint.setStrokeWidth(4);
		 paint.setAntiAlias(true);
		 // 線を描画
		 bmpCanvas.drawLine(oldpos.x, oldpos.y, cur.x, cur.y, paint);
		 oldpos = cur;
		 // 指を持ち上げたら座標をリセット
		 if (event.getAction() == MotionEvent.ACTION_UP) {
			 oldpos = new Point(-1, -1);
		 }
		 invalidate();
		 return true;
	 }
	//--------------------------------------------------------
	// 保存パスの取得処理
	public File getSavePath() {
		File path;
		if (hasSDCard()) { // SDカードがあればそこを保存ディレクトリとする
			path = new File(getSDCardPath() + "/DrawNoteK/");
			path.mkdir();
		} else {
			path = Environment.getDataDirectory();
		}
		return path;
	}
	public String getCacheFilename() {
		File f = getSavePath();
		return f.getAbsolutePath() + "/cache.png";
	}
	public String getSaveFilename() {
		File path = getSavePath();
		Date d = new Date();
		String fname = path.getAbsolutePath() + "/";
		fname += String.format("%4d%02d%02d-%02d%02d%02d.png",
				(1900+d.getYear()), d.getMonth()+1, d.getDate(),
				d.getHours(), d.getMinutes(), d.getSeconds());
		return fname;
	}
	public boolean loadFromFile(String filename) {
		try {
			File f = new File(filename);
			if (!f.exists()) { return false; }
			Bitmap tmp = BitmapFactory.decodeFile(filename);
			bmpCanvas.drawColor(Color.WHITE);
			bmpCanvas.drawBitmap(tmp, 0, 0, null);
			return true;
		} catch (Exception e) {
			return false;
		}
	}
	public void loadFromCacheFile() {
		this.loadFromFile(getCacheFilename());
	}
	public void saveToCacheFile() {
		this.saveToFile(getCacheFilename());
	}
	public void saveToFile() {
		this.saveToFile(getSaveFilename());
	}
	public void saveToFile(String filename) { // 画像をファイルに書き込む
		try {
			FileOutputStream out = new FileOutputStream(filename);
			bmp.compress(CompressFormat.PNG, 100, out);
			out.flush(); out.close();
		} catch(Exception e) {}
	}
	//--------------------------------------------------------
	// SDカードの確認処理
	public boolean hasSDCard() { // SDカードがあるか?
		String status = Environment.getExternalStorageState();
		return status.equals(Environment.MEDIA_MOUNTED);
	}
	public String getSDCardPath() {
		File path = Environment.getExternalStorageDirectory();
		return path.getAbsolutePath();
	}
}

まとめ

以上、Android のライフサイクルを考慮して、手書きメモに画面の一時退避機能を付けてみました。それから、蛇足ですが、線を描画するときのスタイルに「paint.setAntiAlias(true)」をつけると、線のギザギザ感を消すことができました。(その分、ファイルサイズが増えますが…)

ところで、SDカードをマウントしている場合、手書きメモとして保存したファイルを見るには、別途ファイルビューワーをインストールし、それでみるしかありません。そこで、次回は、この点を改善して、AndroidMarketで公開してみたいと思います。

Series Navigation«Android で遊べるスクリプト環境 ASE で遊ぶ«手書きメモを作るAndroid Market で作品を公開しよう!»

執筆者紹介

クジラ飛行机

クジラ飛行机

くじらはんど(http://kujirahand/)にて、日本語プログラミング言語「なでしこ」(IPA未踏ユース採択)、テキスト音楽「サクラ」(OSPオンラインソフト大賞入賞)など多くのオンラインソフトを開発。著書に「Flexプロフェッショナルガイド」「なでしこ公式バイブル」、「一週間でマスターするActionScript3.0」など。

TrackBack URL :