[Androidアプリ開発] タッチでお絵かきしてみた(undo付)

2015.07.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今回は、Android で高速で描画できるらしい SurfaceView を使って、タッチでお絵かきできるサンプルアプリを作ってみようと思います。

作る機能はシンプルに以下の4点です。

  • タッチで線を引く
  • undo機能
  • redo機能
  • リセット機能

ちなみにiOS版については、 [iOSアプリ開発] タッチでお絵かきしてみる を参考にしていただければと思います。

実装

お絵かきできるSurfaceViewの実装

まず、SurfaceView を継承して絵を描画するクラス DrawSurfaceView.java を作ります。

DrawSurfaceView.java

package jp.classmethod.sampletouchdrawing;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff.Mode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

public class DrawSurfaceView extends SurfaceView implements Callback {

	private SurfaceHolder mHolder;
	private Paint mPaint;
	private Path mPath;
	private Bitmap mLastDrawBitmap;
	private Canvas mLastDrawCanvas;

	public DrawSurfaceView(Context context) {
		super(context);
		init();
	}

	public DrawSurfaceView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	private void init() {
		mHolder = getHolder();

		// 透過します。
		setZOrderOnTop(true);
		mHolder.setFormat(PixelFormat.TRANSPARENT);

		// コールバックを設定します。
		mHolder.addCallback(this);

		// ペイントを設定します。
		mPaint = new Paint();
		mPaint.setColor(Color.BLACK);
		mPaint.setStyle(Paint.Style.STROKE);
		mPaint.setStrokeCap(Paint.Cap.ROUND);
		mPaint.setAntiAlias(true);
		mPaint.setStrokeWidth(6);
	}

	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		// 描画状態を保持するBitmapを生成します。
		clearLastDrawBitmap();
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width,
			int height) {
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		mLastDrawBitmap.recycle();
	}

	private void clearLastDrawBitmap() {
		if (mLastDrawBitmap == null) {
			mLastDrawBitmap = Bitmap.createBitmap(getWidth(), getHeight(),
					Config.ARGB_8888);
		}

		if (mLastDrawCanvas == null) {
			mLastDrawCanvas = new Canvas(mLastDrawBitmap);
		}

		mLastDrawCanvas.drawColor(0, Mode.CLEAR);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			onTouchDown(event.getX(), event.getY());
			break;

		case MotionEvent.ACTION_MOVE:
			onTouchMove(event.getX(), event.getY());
			break;

		case MotionEvent.ACTION_UP:
			onTouchUp(event.getX(), event.getY());
			break;

		default:
		}
		return true;
	}

	private void onTouchDown(float x, float y) {
		mPath = new Path();
		mPath.moveTo(x, y);
	}

	private void onTouchMove(float x, float y) {
		mPath.lineTo(x, y);
		drawLine(mPath);
	}

	private void onTouchUp(float x, float y) {
		mPath.lineTo(x, y);
		drawLine(mPath);
		mLastDrawCanvas.drawPath(mPath, mPaint);
	}

	private void drawLine(Path path) {
		// ロックしてキャンバスを取得します。
		Canvas canvas = mHolder.lockCanvas();

		// キャンバスをクリアします。
		canvas.drawColor(0, Mode.CLEAR);

		// 前回描画したビットマップをキャンバスに描画します。
		canvas.drawBitmap(mLastDrawBitmap, 0, 0, null);

		// パスを描画します。
		canvas.drawPath(path, mPaint);

		// ロックを外します。
		mHolder.unlockCanvasAndPost(canvas);
	}

}

L85~ の onTouchEventメソッド でイベントからタッチした座標を取得して描画処理を行っています。 L120~L135 で、SurfaceView のキャンバスに線を描画しています。

undoとredoとリセット機能の実装

次に、undoとredoとリセット機能を実装します。

undo用とredo用の Path を格納するスタックを、フィールドに ArrayDeque で作っておきます。

DrawSurfaceView.java

public class DrawSurfaceView extends SurfaceView implements Callback {

	private SurfaceHolder mHolder;
	private Paint mPaint;
	private Path mPath;
	private Bitmap mLastDrawBitmap;
	private Canvas mLastDrawCanvas;
	private Deque<Path> mUndoStack = new ArrayDeque<Path>();
	private Deque<Path> mRedoStack = new ArrayDeque<Path>();

	public DrawSurfaceView(Context context) {
		super(context);
		init();
	}

タッチした指を離したタイミングで、undoスタックにパスを追加し、redoスタックを空にします。

DrawSurfaceView.java

	private void onTouchUp(float x, float y) {
		mPath.lineTo(x, y);
		drawLine(mPath);
		mLastDrawCanvas.drawPath(mPath, mPaint);
		mUndoStack.addLast(mPath);
		mRedoStack.clear();
	}

次にundoとredoとリセット機能を外のクラスから呼び出せるように、それぞれ publicメソッド で新規作成します。 各メソッドは、undoスタックとredoスタックにパスを出し入れして描画処理を行うシンプルな流れになっています。 以下の、undoメソッドと、redoメソッドと、resetメソッドを追記します。

DrawSurfaceView.java

	public void undo() {
		if (mUndoStack.isEmpty()) {
			return;
		}

		// undoスタックからパスを取り出し、redoスタックに格納します。
		Path lastUndoPath = mUndoStack.removeLast();
		mRedoStack.addLast(lastUndoPath);

		// ロックしてキャンバスを取得します。
		Canvas canvas = mHolder.lockCanvas();

		// キャンバスをクリアします。
		canvas.drawColor(0, Mode.CLEAR);

		// 描画状態を保持するBitmapをクリアします。
		clearLastDrawBitmap();

		// パスを描画します。
		for (Path path : mUndoStack) {
			canvas.drawPath(path, mPaint);
			mLastDrawCanvas.drawPath(path, mPaint);
		}

		// ロックを外します。
		mHolder.unlockCanvasAndPost(canvas);
	}

	public void redo() {
		if (mRedoStack.isEmpty()) {
			return;
		}

		// redoスタックからパスを取り出し、undoスタックに格納します。
		Path lastRedoPath = mRedoStack.removeLast();
		mUndoStack.addLast(lastRedoPath);

		// パスを描画します。
		drawLine(lastRedoPath);

		mLastDrawCanvas.drawPath(lastRedoPath, mPaint);
	}

	public void reset() {
		mUndoStack.clear();
		mRedoStack.clear();

		clearLastDrawBitmap();

		Canvas canvas = mHolder.lockCanvas();
		canvas.drawColor(0, Mode.CLEAR);
		mHolder.unlockCanvasAndPost(canvas);
	}

画面レイアウトとMainActivityの実装

描画周りの機能は完成したので、画面レイアウトとMainActivityを作っていきます。 画面は以下のようにシンプルに作ります。

android-touch-drawing-undo_01

画面レイアウトxmlはこちら。

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <jp.classmethod.sampletouchdrawing.DrawSurfaceView
        android:id="@+id/canvasView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/undoBtn" />

    <Button
        android:id="@+id/undoBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:text="undo" />

    <Button
        android:id="@+id/redoBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/undoBtn"
        android:text="redo" />

    <Button
        android:id="@+id/resetBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:text="reset" />

</RelativeLayout>

画面レイアウトができたので、各ボタンタップ時の処理をMainActivityに実装します。

MainActivity.java

package jp.classmethod.sampletouchdrawing;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends FragmentActivity implements OnClickListener {

	private DrawSurfaceView mCanvasView;
	private Button mUndoBtn;
	private Button mRedoBtn;
	private Button mResetBtn;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mCanvasView = (DrawSurfaceView) findViewById(R.id.canvasView);

		mUndoBtn = (Button) findViewById(R.id.undoBtn);
		mUndoBtn.setOnClickListener(this);

		mRedoBtn = (Button) findViewById(R.id.redoBtn);
		mRedoBtn.setOnClickListener(this);

		mResetBtn = (Button) findViewById(R.id.resetBtn);
		mResetBtn.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		if (v == mUndoBtn) {
			mCanvasView.undo();

		} else if (v == mRedoBtn) {
			mCanvasView.redo();

		} else if (v == mResetBtn) {
			mCanvasView.reset();
		}
	}

}

これでサンプルアプリは完成です。

動作確認

では、お絵かきしてみまーす。

android-touch-drawing-undo_02

はい、上手に描けました!線をなめらかにする処理はまた今度!

ではでは。