オフスクリーン・キャンバスの利用によるお絵かきソフトの開発
これまで数回に分けて、Canvas の利用方法を説明してきました。
「View 派生クラスで Canvas に描画する基礎」ではそもそもキャンバス (Canvas) を利用した描画の種類や 描画を行なうのに必要な構成要素を説明しました。
「タップ時に Canvas に矩形を描画する単純なサンプル」では、View の派生クラスにて矩形を描画する例を紹介しました。
そして「Canvas と Path による手書き View の簡単な実装」では、単純な手書きのお絵かきプログラムの作り方について説明しました。
今回はその手書きのお絵かきプログラムを少し発展させます。
まず、今回作るプログラムは次のようなものです。
画面左上に色を選択するアクションバー・ドロップダウンがあり、そこで選択した色で手書きの線が描ける、というものです。
尚、アクションバー内のドロップダウン(スピナー)については、「アクションバー内のドロップダウン(スピナー)」を参考にしてください。ここでは詳しく説明しません。
前回のプログラムのどこが問題か?今回何を変更するのか?
上記スクリーンショットのプログラムのコードを示す前に、「Canvas と Path による手書き View の簡単な実装」で作成したプログラムでの問題点について説明します。
以前のプログラムでは、Path オブジェクトに描画する線の情報を全て保持していました。そして、 onDraw メソッドのコールバック時に、渡された Canvas オブジェクトに対して、その複数の線を drawPath メソッドで一度に描きだしていました。
この方法では、drawPath メソッドに渡した Paint オブジェクトに指定してある一色でしか描画できません。
今回は複数の色を使えるようにするために、画面に表示されないキャンバスを用意します。これをオフスクリーン・キャンバス (off-screen canvas) とか、あるいはそれに関連付くビットマップをひっくるめてバック・バッファ (back buffer) などと呼んだりします。
描画する Path をオフスクリーン・キャンバスにも描画しておきます。そして次回の Path 描画時には、前回までに描かれた Path については、オフスクリーン・キャンバス(に関連付くビットマップ)から描画して、 新しく描画する内容を直接 drawPath メソッドで描画します。
そうすることで、最新の Paint オブジェクトが保持する色情報で新しいパスを描画し、過去に描いたパスは過去に使った色情報のまま保持されることになります。
したがって、今回の変更はバック・バッファの保持とそれへの描画というところがポイントになります。
改良版お絵かきソフトのコード
MainActivity.java は次のようになります。変更は主にアクションバー内のドロップダウンによるものです。
package com.example.canvastest3;
import android.app.ActionBar.OnNavigationListener;
import android.app.Activity;
import android.app.ActionBar;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.ArrayAdapter;
public class MainActivity extends Activity
implements OnNavigationListener {
CanvasTest3View view;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view = new CanvasTest3View(this);
setContentView(view);
getActionBar().setDisplayShowTitleEnabled(false);
getActionBar().setNavigationMode(
ActionBar.NAVIGATION_MODE_LIST);
ArrayAdapter<CharSequence> adapter
= ArrayAdapter.createFromResource(
this,
R.array.color_array,
R.layout.actionbar_spinner);
adapter.setDropDownViewResource(
R.layout.actionbar_spinner_dropdown);
getActionBar().setListNavigationCallbacks(adapter, this);
}
@Override
public boolean onNavigationItemSelected(
int itemPosition, long itemId) {
int color = Color.WHITE;
switch(itemPosition){
case 0:
color = Color.BLUE;
break;
case 1:
color = Color.GREEN;
break;
case 2:
color = Color.RED;
break;
}
view.setColor(color);
return true;
}
}
View クラスに setColor メソッドを追加して、メニューで選択された色情報を View に渡しています。
ここで res/values/strings.xml は次の通り。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">CanvasTest3</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string-array name="color_array">
<item>Blue</item>
<item>Green</item>
<item>Red</item>
</string-array>
</resources>
res/layout/actionbar_spinner.xml は次の通り。
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="7dp"
android:textAppearance=
"@android:style/TextAppearance.DeviceDefault.Medium.Inverse"
android:textColor="#FFFFFF"/>
res/layout/actionbar_spinner_dropdown.xml は次の内容。
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:height="40dp"
android:paddingLeft="10dp"
android:paddingRight="40dp"
android:textSize="18sp"
android:textColor="#FFFFFF"/>
View クラスは次のようになります。
package com.example.canvastest3;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.view.MotionEvent;
import android.view.View;
public class CanvasTest3View extends View {
private Canvas canvas;
private Bitmap bitmap;
private Paint paint;
private Path path;
public CanvasTest3View(Context context) {
super(context);
path = new Path();
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(10);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(bitmap, 0, 0, null);
canvas.drawPath(path, paint);
}
@Override
protected void onSizeChanged(
int w, int h,
int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
bitmap = Bitmap.createBitmap(
w, h, Bitmap.Config.ARGB_8888);
canvas = new Canvas(bitmap);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
path.reset();
path.moveTo(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
path.lineTo(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
path.lineTo(x, y);
canvas.drawPath(path, paint);
path.reset();
invalidate();
break;
}
return true;
}
public void setColor(int color){
paint.setColor(color);
}
}
ACTION_UP で canvas の drawPath を呼んでいますが、ここでオフスクリーン・キャンバス(を通して、それに紐付くビットマップ)にパスを描画しています。
その直後パスをリセットしていますから、invalidate を呼び出し後の onDraw では drawPath では何も描画されず、全てオフスクリーンキャンバスのビットマップから描画されることになります。