トップ » 技術記事 » Android で再開する Java プログラミング(8) - 手書きメモを作る

Android で再開する Java プログラミング(8) - 手書きメモを作る

タグ: Android

皆様、Android ライフはどうでしょうか。私はなかなか楽しんでおります。というのも、これまで通勤の電車の中では、音楽 or 読書 or メモだったのですが、Android なら、前者2つはどちらも満足して使えます。(ちなみにメモには、ポメラを使っています。)

音楽は、SDカードにコピーするだけですし、読書も、aFile Lite というビューワーアプリで、SDカードに入れたテキストを快適に読むことができます。(文字のサイズや文字コード、背景色などを選べるので快適に読書できます。)

さて、今回は、Android で手書きメモを作ってみます。

対象とする読者

  • Android アプリを作って見たい人
  • Java で簡単なプログラムが組める人

どんなアプリケーションを作るのか?

とにかくシンプルな手書きメモを作ってみます。手書きメモは、保存できるようにしています。

http://aoikujira.com/demo/hakkaku/rc/20090806t30eUG-tegakimemo.png

本稿では、プログラムを段階をおって作っていきます。

手順(1)プロジェクトの作成

本稿では、既に、Android SDK がインストールしてあるものとして話を進めます。インストールしてない方は、こちらを参考にインストールしてみてください。

Eclipse を起動したら、メインメニューより[File > New > Project…]をクリックします。続いて、[Android > Android Project]を選択して、[Next]をクリックします。

以下のような設定ダイアログが出たら、プロジェクトに名前を付けます。

http://aoikujira.com/demo/hakkaku/rc/20090805cxLkAz-CreateProject.png
項目 設定する値
Project name DrawNoteK
Build Target Android 1.5
Application name DrawNoteK
Project name com. kujirahand.android.drawnotek
Create Activity (チェック) DrawNoteK
Min SDK Version 3

手順(2)描画のテストを行う

Eclipse でプロジェクトのメインファイル「DrawNoteK.java」を開きます。そして、まずは、プログラムの雛形を組み立てましょう。以下のプログラムは、独自定義した View クラスに対して青い四角形を描画を行うという単純なものです。

package com.kujirahand.android.drawnotek;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;

/** メインクラスの定義 */
public class DrawNoteK extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 描画クラスを設定
        DrawNoteView view = new DrawNoteView(getApplication());
        setContentView(view);
    }
}
/** 描画クラスの定義 */
class DrawNoteView extends android.view.View {
    public DrawNoteView(Context c) {
        super(c);
        setFocusable(true);
    }
    /** 描画を行うイベント */
    public void onDraw(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
        Paint paint = new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(new Rect(0,0,30,30), paint);
    }
}

実行するためには、メインメニューから[Run > Debug] をクリックし、表示される画面で、[Android Application]を選択して、[OK]ボタンをクリックします。これを実行すると、次のように表示されます。

http://aoikujira.com/demo/hakkaku/rc/20090805qZ1E9U-emu-blue-rect.png

素晴らしいことに、実機をUSBで接続していれば、同じように表示されます。以下は、HT-03Aで実行してみたところです。

http://aoikujira.com/demo/hakkaku/rc/20090805Zbccu-jikki-blue-rect.png

もし、実機にうまく接続できない場合には、(実機で動かしてみよう)を見て、USBドライバの再インストールを行って下さい。

手順(3)指タッチに応じて描画行うようにしよう

描画テストができたら、雛形に指でタッチした部分に円を描画するというプログラムを追加してみましょう。DrawNoteK.java を以下のように書き換えます。

package com.kujirahand.android.drawnotek;

import java.util.ArrayList;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;
import android.view.MotionEvent;

/** メインクラスの定義 */
public class DrawNoteK extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 描画クラスを設定
        DrawNoteView view = new DrawNoteView(getApplication());
        setContentView(view);
    }
}
/** 描画クラスの定義 */
class DrawNoteView extends android.view.View {
	ArrayList<Point> draw_list = new ArrayList<Point>();
	public DrawNoteView(Context c) {
		super(c);
		setFocusable(true);
	}
	/** 描画イベント */
	public void onDraw(Canvas canvas) {
		canvas.drawColor(Color.WHITE);
		Paint paint = new Paint();
		paint.setColor(Color.BLUE);
		paint.setStyle(Paint.Style.FILL);
		for (int i = 0; i < draw_list.size(); i++) { // 記憶された座標を次々と描画
			Point p = draw_list.get(i);
			canvas.drawCircle(p.x, p.y, 4, paint);
		}
	}
	/** タッチイベント */
	 public boolean onTouchEvent(MotionEvent event) {
		 int x = (int)event.getX();
		 int y = (int)event.getY();
		 draw_list.add(new Point(x, y)); // ArrayList に描画する座標を記憶
		 invalidate(); // 再描画の指示
		 return true;
	 }
}

これを実行すると、以下のようになります。適当にタッチしてみますと、タッチした場所に青色の円が描かれます。

http://aoikujira.com/demo/hakkaku/rc/20090805QMIOo-touchEvent.png

ここでは、タッチした座標を ArrayList に覚えておいて、描画イベントで描画し直すという仕組みを作って見ました。ポイントとなるのは、onTouchEvent() イベントで、ここにおいて、ユーザーがタッチした座標を ArrayList に追加します。

ところで、onTouchEvent() の中で描画を行おうと思っても、ここでは描画を行うことはできません。このイベントの中では、再描画を行うようにという指示を invalidate() メソッドを呼ぶことにより行います。そして、実際に描画を行うのは、onDraw() で行います。

手順(4)描画方法を改善しよう

さて、手順3までで作ったものは、タッチした箇所にしか円が描かれないので、ボツボツの状態で格好良いとは言えないものでした。そこで、もう少し、スムーズに見えるように、タッチイベントが起きた座標を線でつないで、もう少し見栄えがよいものになるよう改造してみます。

package com.kujirahand.android.drawnotek;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;
import android.view.MotionEvent;

/** メインクラスの定義 */
public class DrawNoteK extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 描画クラスを設定
        DrawNoteView view = new DrawNoteView(getApplication());
        setContentView(view);
    }
}
/** 描画クラスの定義 */
class DrawNoteView extends android.view.View {
	ArrayList<Point> draw_list = new ArrayList<Point>();
	public DrawNoteView(Context c) {
		super(c);
		setFocusable(true);
	}
	/** 描画イベント */
	protected void onDraw(Canvas canvas) {
		canvas.drawColor(Color.WHITE);
		Paint paint = new Paint();
		paint.setColor(Color.BLUE);
		paint.setStyle(Paint.Style.FILL);
		paint.setStrokeWidth(4);
		// 記録した座標を順に繋げて描画する
		Point q = new Point(-1,-1);
		for (int i = 0; i < draw_list.size(); i++) {
			Point p = draw_list.get(i);
			if (p.x >= 0) {
				if (q.x < 0) { q = p; }
				canvas.drawLine(q.x, q.y, p.x, p.y, paint);
			}
			q = p;
		}
	}
	/** タッチイベント */
	 public boolean onTouchEvent(MotionEvent event) {
		 int x = (int)event.getX();
		 int y = (int)event.getY();
		 draw_list.add(new Point(x, y));
		 if (event.getAction() == MotionEvent.ACTION_UP) {
			 // 無効な値を与えて線が切れる旨を指示
			 draw_list.add(new Point(-1, -1));
		 }
		 invalidate();
		 return true;
	 }
}

どうでしょうか。これで実行してみると、そこそこ手書きメモとしては、良い感じになってきました。

http://aoikujira.com/demo/hakkaku/rc/20090805peYWhg-draw-line.png

手順(3)からの改良点は、点と点を線で繋げるようにしたことです。そのために、指をパネルから離した時に、座標(-1,-1)という無効な値を記録するようにしています。そして、描画イベントの中で、-1,-1というイベントがあれば、そこを線の開始点とみなすようにしています。

手順(5)画面のクリアメニューを追加する

そして、次に画面のクリアメニューを追加しています。Android では、本体にある[Menu]ボタンを押したら、何かしらのメニューを表示するというのが基本的な動作となっています。そこで、画面のクリアも、メニューで実行できるようにします。

下記のように、DrawNoteK.java を書き換えます。ここでは、画面を消去する Clear メニューのみを実装しています。

package com.kujirahand.android.drawnotek;

import java.util.ArrayList;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;
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: // TODO
    		break;
    	}
    	return true;
    }
}
/** 描画クラスの定義 */
class DrawNoteView extends android.view.View {
	ArrayList<Point> draw_list = new ArrayList<Point>();
	public DrawNoteView(Context c) {
		super(c);
		setFocusable(true);
	}
	public void clearDrawList() {
		draw_list.clear();
		invalidate();
	}
	/** 描画イベント */
	protected void onDraw(Canvas canvas) {
		canvas.drawColor(Color.WHITE);
		Paint paint = new Paint();
		paint.setColor(Color.BLUE);
		paint.setStyle(Paint.Style.FILL);
		paint.setStrokeWidth(4);
		// 記録した座標を順に繋げて描画する
		Point q = new Point(-1,-1);
		for (int i = 0; i < draw_list.size(); i++) {
			Point p = draw_list.get(i);
			if (p.x >= 0) {
				if (q.x < 0) { q = p; }
				canvas.drawLine(q.x, q.y, p.x, p.y, paint);
			}
			q = p;
		}
	}
	/** タッチイベント */
	public boolean onTouchEvent(MotionEvent event) {
		 int x = (int)event.getX();
		 int y = (int)event.getY();
		 draw_list.add(new Point(x, y));
		 if (event.getAction() == MotionEvent.ACTION_UP) {
			 // 無効な値を与えて線が切れる旨を指示
			 draw_list.add(new Point(-1, -1));
		 }
		 invalidate();
		 return true;
	 }
}

実行すると、以下のようにメニューがでます。

http://aoikujira.com/demo/hakkaku/rc/200908052RIpsM-optionsMenu.png

ほとんどの部分が同じですが、メインクラスの DrawNoteK に、メニューの追加と、メニューがクリックされた時の処理を加えています。メニューの作成のためには、「onCreateOptionsMenu()」メソッドと「onCreateOptionsMenu()」を定義します。それぞれ、メニューの作成と、メニューがクリックされた時の処理を記述します。

手順(6)画像を保存する

メニューを実装したところで、画像を保存するようにしましょう。画像を保存するには、Bitmap クラスを利用します。そこで、Bitmap を使って描画を行うように描画方法も修正してみました。

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.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.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;
    }
}
/** 描画クラスの定義 */
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();
	}
	public void saveToFile() {
		// 保存先の決定
		String status = Environment.getExternalStorageState();
		File fout;
		if (!status.equals(Environment.MEDIA_MOUNTED)) {
			fout = Environment.getDataDirectory();
		} else {
			fout = new File("/sdcard/DrawNoteK/");
			fout.mkdirs();
		}
		Date d = new Date();
		String fname = fout.getAbsolutePath() + "/";
		fname += String.format("%4d%02d%02d-%02d%02d%02d.png",
				(1900+d.getYear()), d.getMonth(), d.getDate(),
				d.getHours(), d.getMinutes(), d.getSeconds());
		// 画像をファイルに書き込む
		try {
			FileOutputStream out = new FileOutputStream(fname);
			bmp.compress(CompressFormat.PNG, 100, out);
			out.flush(); out.close();
		} catch(Exception e) {}
	}
	/** 画面サイズが変更された時 */
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w,h,oldw,oldh);
		bmp = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
		bmpCanvas = new Canvas(bmp);
		bmpCanvas.drawColor(Color.WHITE);
	}
	/** 描画イベント */
	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);
		 // 線を描画
		 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;
	 }
}

実行してみると、みごと、SDカードの中に保存することができました。

http://aoikujira.com/demo/hakkaku/rc/20090806YAhX4A-save1.png
http://aoikujira.com/demo/hakkaku/rc/20090806aPH67c-save2.png

まとめ

以上、Android で手書きメモを作ってみました。メモを書いて画像を保存するだけなので、シンプルですが、APIの使い方や、Bitmap と Canvas の関係などいくつかツボを押さておく必要があります。

また、ここまでの手順で作ったもので完成と思ったのですが、画面の向きを変えるなど、何かしらの割り込みが起きたとき画面が初期化されてしまうという問題があります。そこで、次回、状態の保存と復元の処理を追加します。

※ちなみに、「CTRL」+「F11」キーを押すと、エミュレーターの画面の向きが切り替わりますので、動作を確認してみてください。

参考資料

Series Navigation«基本的なUIをマスターしよう«docomo HT-03A を入手~実機で Android アプリを動かしてみようAndroid ライフサイクルを考慮-手書きメモを作り込む»

執筆者紹介

クジラ飛行机

クジラ飛行机

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

TrackBack URL :