こんにちは、コンテンツクリエイターのともすけです。
(ごめんなさいね、続きがなくて。こういうのを企画倒れといいます…。でも、そのうち続きを作りたいです)
「ステップバイステップで学ぶ」シリーズを始めます。このシリーズは、いきなりギュッと詰め込んだ話をしない(つもり)で、第一段階、第二段階…と順に進めていき、試しながら理解度を深めていただこうというシリーズです。
コードはコピペで動きます。各ステップのコードは「単体」で動きます。ただし、以下はご自分で対応してください。
- Android Studioで、適当なプロジェクトを自分で作成する
- 作成したら、ひな形のMainActivity.javaが生成されているはずなので、その「1行目」以外を削除して以下に掲載したコードを貼り付ける
- Runボタンを押して、AVD(Android Virtual Device)もしくは自前のAndroid端末にインストールする
↑の内容がわからない方は、この記事ではサポートできませんが、別記事で説明できれば良いなぁと思っています。
以下の記事の元は、ぼくがGoogleサイトで公開しているモノなのですが、コードが見づらいので修正して更新しようとすると、Googleが「変更差分がない」と勘違いして上げられなくて、仕方なく少しずつこちらへ引っ越そうと思います。
図形描画について
知っている範囲では、描画には2通りあります。
高速な描画処理を行いたい場合は2番を、それ以外は1番を使うのがいいようです。今回はお絵かきをターゲットとしているので、1番のViewクラスを使います。
ステップ1.Viewの基本
Viewは、文字を書いたり、線を書いたり、塗りつぶしたりすることのできるオブジェクトです。Viewクラスを継承したclassを定義(ここではCustomView)して
以下のコード例では、
を描画しています。setStyleやsetColorは、同じであれば再設定せずにcanvasへ書き込みできます。
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new CustomView(this));
}
class CustomView extends View {
public CustomView(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
}
@Override
protected void onDraw(Canvas canvas){
Paint paint = new Paint();
// テキスト書き込み
paint.setAntiAlias(true);
paint.setTextSize(24);
paint.setColor(Color.rgb(0, 0, 0));
canvas.drawText("どろーてきすと", 0, 60, paint);
// ラインの書き込み
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLUE);
canvas.drawLine(0, 100, 100, 200, paint);
// 四角形の描画(塗りつぶし)
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.GREEN);
canvas.drawRect(0, 200, 100, 300, paint);
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
canvas.drawCircle(50, 350, 50, paint);
}
}
}
ステップ2.タッチ操作で書き込み
ではさっそく、タッチ操作で書き込みしてみたいと思います。画面をタッチすると、その座標を取得できます。1座標だけなら上記例の通り、drawTextやdrawCircleで書けます。やってみましょう。
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new CustomView(this));
}
class Pointer {
float x,y;
public Pointer(float x, float y){
this.x = x;
this.y = y;
}
}
class CustomView extends View {
protected Pointer pointer = null;
public CustomView(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
}
@Override
protected void onDraw(Canvas canvas){
Paint paint = new Paint();
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
if(pointer!=null){
canvas.drawCircle(pointer.x, pointer.y, 30, paint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event){
int action = event.getAction();
switch(action & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_MOVE:
pointer = new Pointer(event.getX(), event.getY());
}
invalidate();
return true;
}
}
}
ステップ3.軌跡を残す
軌跡を残すには、座標情報をすべて保持して、常にすべて再描画する方法があります。ちょっとしたお絵かきなら、パフォーマンス的な問題をほとんど感じません。ですが、いっぱいいっぱい描いてあげると、座標情報が多すぎるため、処理がもたつき始めます。この「もたつく」ところは面白いので、実現してみようと思います。
変更点ですが、
画面に軌跡が保持されますが、何度も軌跡を描いていると、次第に丸の表示速度が遅くなることがわかると思います。逆に言えば、数百個程度の座標であれば瞬時に描画してしまうパワーをAndroid端末は持っていることもわかると思います。(端末によって差異があります)
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new CustomView(this));
}
class Pointer {
float x, y;
public Pointer(float x, float y) {
this.x = x;
this.y = y;
}
}
class CustomView extends View {
protected ArrayList<Pointer> pointerList = null; //(2)
Paint paint = null; //(1)
public CustomView(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
pointerList = new ArrayList<Pointer>(); //(2)
paint = new Paint(); //(1)
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE); //(1)
paint.setColor(Color.RED); //(1)
}
@Override
protected void onDraw(Canvas canvas) {
if (pointerList != null) {
for (Pointer pointer : pointerList) { //(2)
canvas.drawCircle(pointer.x, pointer.y, 30, paint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
pointerList.add(new Pointer(event.getX(), event.getY())); //(2)
invalidate(); //(3)
}
return true;
}
}
}
ステップ4.描画処理を軽減する
ステップ3では、ポインターの情報を配列に格納して、それを「毎回はじめから」描き直しています。Undo実装なら、まあかなり目をつむって「アリ」なのかもしれませんが、とりあえずお絵かきするにはかなり冗長な処理です。ここでは処理方法を変えたいと思います。
これまでの考え方は、
でした。
で、今回は以下のようになります。
今回も、onTouchEventごとに画面を更新していきますが、ステップ3の「全配列描画」と異なり、描画にかかる処理負荷は一定になります。毎回「仮空間のキャンバスを、端末の表示用キャンバスに書き写す」わけですから。
変更箇所としては
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new CustomView(this));
}
/*class Pointer { ここが不要になって
float x,y;
public Pointer(float x, float y){
this.x = x;
this.y = y;
}
}*/
class CustomView extends View {
//protected ArrayList<Pointer> pointerList = null; これも不要になって
Paint paint = null;
Bitmap bmp;
Canvas bmpCanvas;
public CustomView(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
//pointerList = new ArrayList<Pointer>(); これも不要になって
paint = new Paint();
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
}
@Override
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);
}
@Override
protected void onDraw(Canvas canvas){
/*if(pointerList!=null){ これも不要になって
for(Pointer pointer : pointerList){
canvas.drawCircle(pointer.x, pointer.y, 5, paint);
}
}*/
canvas.drawBitmap(bmp, 0, 0, null); //②
}
@Override
public boolean onTouchEvent(MotionEvent event){
int action = event.getAction();
switch(action & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_MOVE:
//pointerList.add(new Pointer(event.getX(), event.getY())); これも不要に
bmpCanvas.drawCircle(event.getX(), event.getY(), 30, paint); //③
invalidate();
}
return true;
}
}
}
おまけ:上記4つのモードを切り替えて動かせるコード
説明しませんが、よかったらお試しでコピペして遊んでみてください。どんな風に動くものか、動画も撮りましたので合わせてみてみてください。
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity {
private final int WC = LinearLayout.LayoutParams.WRAP_CONTENT;
private final int MP = LinearLayout.LayoutParams.MATCH_PARENT;
private TextView selectViewMsg = null;
private LinearLayout viewLayout = null;
private Custom0View cv0 = null;
private Custom1View cv1 = null;
private Custom2View cv2 = null;
private Custom3View cv3 = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
LinearLayout baseLayout = new LinearLayout(this);
baseLayout.setOrientation(LinearLayout.VERTICAL);
selectViewMsg = new TextView(this);
selectViewMsg.setText("指定なし");
selectViewMsg.setLayoutParams(new LinearLayout.LayoutParams(MP, WC));
LinearLayout upperLayout = new LinearLayout(this);
upperLayout.setOrientation(LinearLayout.HORIZONTAL);
upperLayout.addView(genButton("ステップ1", 0));
upperLayout.addView(genButton("ステップ2", 1));
upperLayout.addView(genButton("ステップ3", 2));
upperLayout.addView(genButton("ステップ4", 3));
viewLayout = new LinearLayout(this);
cv0 = new Custom0View(this);
cv1 = new Custom1View(this);
cv2 = new Custom2View(this);
cv3 = new Custom3View(this);
baseLayout.addView(selectViewMsg);
baseLayout.addView(upperLayout);
baseLayout.addView(viewLayout);
setContentView(baseLayout);
}
class Custom0View extends View {
public Custom0View(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
}
@Override
protected void onDraw(Canvas canvas){
Paint paint = new Paint();
// テキスト書き込み
paint.setAntiAlias(true);
paint.setTextSize(72);
paint.setColor(Color.rgb(0, 0, 0));
canvas.drawText("どろーてきすと", 0, 60, paint);
// ラインの書き込み
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.BLUE);
canvas.drawLine(0, 100, 500, 600, paint);
// 四角形の描画(塗りつぶし)
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.GREEN);
canvas.drawRect(0, 200, 300, 500, paint);
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
canvas.drawCircle(550, 350, 200, paint);
}
}
class Pointer {
float x,y;
public Pointer(float x, float y){
this.x = x;
this.y = y;
}
}
class Custom1View extends View {
protected Pointer pointer = null;
public Custom1View(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
}
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
if (pointer != null) {
canvas.drawCircle(pointer.x, pointer.y, 30, paint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
pointer = new Pointer(event.getX(), event.getY());
}
invalidate();
return true;
}
}
class Custom2View extends View {
protected ArrayList<Pointer> pointerList = null; //(2)
Paint paint = null; //(1)
public Custom2View(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
pointerList = new ArrayList<Pointer>(); //(2)
paint = new Paint(); //(1)
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE); //(1)
paint.setColor(Color.RED); //(1)
}
@Override
protected void onDraw(Canvas canvas) {
if (pointerList != null) {
for (Pointer pointer : pointerList) { //(2)
canvas.drawCircle(pointer.x, pointer.y, 30, paint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE:
pointerList.add(new Pointer(event.getX(), event.getY())); //(2)
invalidate(); //(3)
}
return true;
}
}
class Custom3View extends View {
//protected ArrayList<Pointer> pointerList = null; これも不要になって
Paint paint = null;
Bitmap bmp;
Canvas bmpCanvas;
public Custom3View(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
//pointerList = new ArrayList<Pointer>(); これも不要になって
paint = new Paint();
// 円の描画(線)
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.RED);
}
@Override
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);
}
@Override
protected void onDraw(Canvas canvas){
/*if(pointerList!=null){ これも不要になって
for(Pointer pointer : pointerList){
canvas.drawCircle(pointer.x, pointer.y, 5, paint);
}
}*/
canvas.drawBitmap(bmp, 0, 0, null); //②
}
@Override
public boolean onTouchEvent(MotionEvent event){
int action = event.getAction();
switch(action & MotionEvent.ACTION_MASK){
case MotionEvent.ACTION_MOVE:
//pointerList.add(new Pointer(event.getX(), event.getY())); これも不要に
bmpCanvas.drawCircle(event.getX(), event.getY(), 30, paint); //③
invalidate();
}
return true;
}
}
private Button genButton(String msg, int id){
Button button = new Button(this);
button.setText(msg);
button.setLayoutParams(new LinearLayout.LayoutParams(0, WC, 1));
button.setTag(id);
button.setOnClickListener(new ButtonClickListener());
return button;
}
private class ButtonClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
viewLayout.removeAllViews();
switch ((Integer) v.getTag()){
case 0:
viewLayout.addView(cv0);
selectViewMsg.setText("ステップ1");
break;
case 1:
viewLayout.addView(cv1);
selectViewMsg.setText("ステップ2");
break;
case 2:
viewLayout.addView(cv2);
selectViewMsg.setText("ステップ3");
break;
case 3:
viewLayout.addView(cv3);
selectViewMsg.setText("ステップ4");
break;
default :
break;
}
}
}
}
以上になります。ここに示したサンプルは最適なコードではありませんが、こうやってコードを書いたら絵を描けるんだね、じゃあブラシとか自分で作ってみて、それを描くのに使えたりするのかな?とかいう試行錯誤を通してプログラミングのスキルアップに繋がればと思います。
それではまた。
