[Androidアプリ開発] タッチでお絵かきしてみた(undo付)
今回は、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を作っていきます。 画面は以下のようにシンプルに作ります。
画面レイアウト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(); } } }
これでサンプルアプリは完成です。
動作確認
では、お絵かきしてみまーす。
はい、上手に描けました!線をなめらかにする処理はまた今度!
ではでは。