marunomaruno-memo

marunomaruno-memo

[Android][AppInventor] PaintPot の Java 版

2011年12月30日 | Android
[Android][AppInventor] PaintPot の Java 版
================================================================================

AppInventor のチュートリアルのサンプル「PaintPot」を Java を使って実装してみる。

PaintPot は以下のようなアプリ。
---
PaintPot で画面をタッチしてさまざまなカラーのドットや線を描いてらくがきができま
す。
これが PaintPot アプリでできることです。
    描きたい色の仮想ペンキ入れに、指を浸します。
    画面にドラッグした指にそって線が描かれます。
    画面を突いてドットを描きます。
    下のボタンを使って画面をきれいに拭き取ります。
    描画する背景としてイメージが含まれています
---
(ソフトウェア技術ドキュメントを勝手に翻訳 - AppInventor チュートリアル、
http://www.techdoctranslator.com/appinventor/learn/tutorials より)

ただし、オリジナルは、画面の縦横が変わっても線が保持されているが、これは保持して
いない。


今回作ったクラスはつぎの 4 つ。

PaintPotActivity    アクティビティ
CanvasView          カスタム・ビュー(このクラスで描画)
PointWithPaint      Paint オブジェクト付きの点
PathWithPaint       Paint オブジェクト付きの折れ線


今回作ってみて、GUI 系のことは、AppInventor のようなものがあると便利。Java で作
るとなると、わりと面倒、という感じだ。


■ アクティビティ

線の色や太さを変えるボタンとその処理。
今回は、レイアウトの属性 android:onClick 属性にハンドラーを設定したので、このク
ラスの中でハンドラーを定義している。

□ PaintPotActivity
---
package jp.marunomaruno.android.paintpot;

import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

/**
 * AppInventor のサンプル PaintPot の Java 版。
 * アクティビティ
 * @author marunomaruno
 * @version 1.0, 2011-12-25
 */
public class PaintPotActivity extends Activity {

    private CanvasView canvasView;    // (1)

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d("TEST", "PaintPotActivity.onCreate()");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onResume() {
        Log.d("TEST", "PaintPotActivity.onResume()");
        super.onResume();

        canvasView = (CanvasView) findViewById(R.id.canvasView);
    }

    /**
     * 「赤」ボタン・クリック
     * @param v
     */
    public void onRedButtonClick(View v) {
        Log.d("TEST", String.format(
                "PaintPotActivity.onRedButtonClick() - [%s]", v.toString()));
        canvasView.setPaintColor(Color.RED);    // (2)
    }

    /**
     * 「緑」ボタン・クリック
     * @param v
     */
    public void onGreenButtonClick(View v) {
        Log.d("TEST", String.format(
                "PaintPotActivity.onGreenButtonClick() - [%s]", v.toString()));
        canvasView.setPaintColor(Color.GREEN);    // (3)
    }

    /**
     * 「青」ボタン・クリック
     * @param v
     */
    public void onBlueButtonClick(View v) {
        Log.d("TEST", String.format(
                "PaintPotActivity.onBlueButtonClick() - [%s]", v.toString()));
        canvasView.setPaintColor(Color.BLUE);    // (4)
    }

    /**
     * 「クリア」ボタン・クリック
     * @param v
     */
    public void onWipeButtonClick(View v) {
        Log.d("TEST", String.format(
                "PaintPotActivity.onWipeButtonClick() - [%s]", v.toString()));
        canvasView.wipe();    // (5)
    }

    /**
     * 「太線」ボタン・クリック
     * @param v
     */
    public void onHeavyButtonClick(View v) {
        Log.d("TEST", String.format(
                "PaintPotActivity.onHeavyButtonClick() - [%s]", v.toString()));
        canvasView.setStrokeWidth(CanvasView.HEAVY);    // (6)
    }

    /**
     * 「細線」ボタン・クリック
     * @param v
     */
    public void onThinButtonClick(View v) {
        Log.d("TEST", String.format(
                "PaintPotActivity.onThinButtonClick() - [%s]", v.toString()));
        canvasView.setStrokeWidth(CanvasView.THIN);    // (7)
    }
}
---

(1) カスタムビューのオブジェクト

ボタンをクリックしたときに、色を変えたり、線の太さを変えたりするために持っている。

    private CanvasView canvasView;    // (1)


(2)-(7) カスタムビューの属性設定 

それぞれ、ボタンをクリックしたときに行う処理。

    canvasView.setPaintColor(Color.RED);    // (2)
    canvasView.setPaintColor(Color.GREEN);    // (3)
    canvasView.setPaintColor(Color.BLUE);    // (4)
    canvasView.wipe();    // (5)
    canvasView.setStrokeWidth(CanvasView.HEAVY);    // (6)
    canvasView.setStrokeWidth(CanvasView.THIN);    // (7)


■ カスタム・ビュー

描画のキャンパスになっているカスタム・ビューのクラス。
丸や線のデータをリストで保持している。

□ CanvasView.java
---
package jp.marunomaruno.android.paintpot;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.ImageView;

/**
 * AppInventor のサンプル PaintPot の Java 版。 
 * 丸を打つ、線を引くキャンバス。
 * @author marunomaruno
 * @version 1.0, 2011-12-25
 */
public class CanvasView extends ImageView {
    public static final float HEAVY = 15F;    // 太い    // (1)
    public static final float THIN = 5F;    // 細い    // (2)

    private Paint paint;

    private List<PointWithPaint> dots; // ドット用    // (3)
    private List<PathWithPaint> lines; // 軌跡用    // (4)

    public CanvasView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

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

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

    /**
     * 初期化する。
     * @param context
     */
    private void init(Context context) {
        paint = new Paint();
        paint.setStrokeWidth(THIN);
        paint.setAntiAlias(true);

        wipe();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 丸を描画する
        for (PointWithPaint point : dots) {
            Log.v("TEST", "dot = " + point.toString());
            canvas.drawCircle(point.x, point.y, point.paint.getStrokeWidth(),
                    point.paint);
        }

        // 線を描画する
        for (PathWithPaint line : lines) {
            Log.v("TEST", "line = " + line.toString());
            canvas.drawPath(line.path, line.paint);
        }

    }

    // onTouchEvent() だけで使用
    private int prevAction = -1; // 直前のアクション    // (5)
    private Path path = null; // パス    // (6)

    /*
     * (non-Javadoc)
     * @see android.view.View.OnTouchListener#onTouch(android.view.View,
     * android.view.MotionEvent)
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TEST", String.format(
                "CanvasTouchlistener.onTouch() - action = [%d]", event
                        .getAction()));

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:    // (7)
            path = new Path();
            path.moveTo(event.getX(), event.getY());
            break;

        case MotionEvent.ACTION_UP:    // (8)
            if (prevAction == MotionEvent.ACTION_DOWN) {    // (9)
                dots.add(new PointWithPaint(event.getX(), event.getY(), paint));
            }

            lines.add(new PathWithPaint(path, paint));
            invalidate();

            break;

        case MotionEvent.ACTION_MOVE:    // (10)
            path.lineTo(event.getX(), event.getY());

            break;

        default:
            assert true; // 何もしない
            break;

        }

        prevAction = event.getAction();    // (11)

        return true;    // (12)
    }

    /**
     * 指定された色に設定する。
     * @param color 色
     */
    public void setPaintColor(int color) {
        paint.setColor(color);
    }

    /**
     * 指定された線の幅にする。
     * @param width 幅
     */
    public void setStrokeWidth(float width) {
        paint.setStrokeWidth(width);
    }

    /**
     * 描画したものをきれいにする。
     */
    public void wipe() {
        dots = new ArrayList<PointWithPaint>();
        lines = new ArrayList<PathWithPaint>();
        invalidate();
    }
}
---

(1)(2) 線の太さをあらわす定数

    public static final float HEAVY = 15F;    // 太い    // (1)
    public static final float THIN = 5F;    // 細い    // (2)


(3)(4) 軌跡の保持


    private List<PointWithPaint> dots; // ドット用    // (3)
    private List<PathWithPaint> lines; // 折れ線用    // (4)


(5)(6) onTouchEvent() だけで使用するインスタンス変数

ACTION_UP したときにサークルを描くと、ドラッグしているときに描いた折れ線の最後に
サークルが描かれるので、前回のアクションを保持しておくための変数。

    private int prevAction = -1; // 直前のアクション    // (5)


1回の移動で描かれる折れ線を保持する。

    private Path path = null; // パス    // (6)


(7) タッチしたとき

タッチしたときは、新しい折れ線のオブジェクトを生成して、そこをスタート地点にする。

    case MotionEvent.ACTION_DOWN:    // (7)
        path = new Path();
        path.moveTo(event.getX(), event.getY());
        break;


(8)(9) 指が離れたとき

指が離れたときは、その座標を折れ線の最後にする。
また、タッチして、その座標で指を離せば、そこにサークルを描く。

    case MotionEvent.ACTION_UP:    // (8)
        if (prevAction == MotionEvent.ACTION_DOWN) {    // (9)
            dots.add(new PointWithPaint(event.getX(), event.getY(), paint));
        }

        lines.add(new PathWithPaint(path, paint));
        invalidate();


(10) 指を動かしているとき

指を動かしているときは、その座標を折れ線の点とする。

    case MotionEvent.ACTION_MOVE:    // (10)
        path.lineTo(event.getX(), event.getY());


(11) 前回のアクションを保持

    prevAction = event.getAction();    // (11)


(12) アクションは処理済とする

    return true;    // (12)

この返り値が false のままだと、未処理となるので、つぎのイベントを拾えない。
今回は、MotionEvent を拾うハンドラーは、このメソッドなので、true にしておく。


■ データ

描画するデータを保持するクラス。
今回は、色と線の太さも保持しないといけないので、
タッチされた座標と Paint オブジェクトのペアを持つクラス PointWithPaint 
と、
移動した座標群と Paint オブジェクトのペアを持つクラス PathWithPaint 
の 2 つのクラスを作る。


□ PointWithPaint.java
---
package jp.marunomaruno.android.paintpot;

import android.graphics.Paint;
import android.graphics.PointF;

/**
 * AppInventor のサンプル PaintPot の Java 版。 
 * Paintオブジェクトつきの点
 * @author marunomaruno
 * @version 1.0, 2011-12-25
 */
public class PointWithPaint {
    float x;
    float y;
    Paint paint;

    public PointWithPaint(float x, float y, Paint paint) {
        this.x = x;
        this.y = y;
        this.paint = new Paint(paint); // (1)
        this.paint.setStyle(Paint.Style.FILL); // 塗りつぶす // (2)
    }

    public PointWithPaint(PointF point, Paint paint) {
        this(point.x, point.y, paint);
    }

    @Override
    public String toString() {
        return String.format("[%.1f, %.1f, %.1f, %x]", x, y, paint
                .getStrokeWidth(), paint.getColor());
    }
}
---

(1)(2) あたらしいペイント・オブジェクトを生成

あたらしいペイント・オブジェクトとして保持する必要がある。そうでないと、最後に設
定したペイント・オブジェクトの値が描画時に採用されることになる。

    this.paint = new Paint(paint); // (1)

また、円を描くので、ここでは、塗りつぶしのスタイルにする。

    this.paint.setStyle(Paint.Style.FILL); // 塗りつぶす // (2)


□PathWithPaint.java
---
package jp.marunomaruno.android.paintpot;

import android.graphics.Paint;
import android.graphics.Path;

/**
 * AppInventor のサンプル PaintPot の Java 版。 
 * Paintオブジェクトつきの折れ線。
 * @author marunomaruno
 * @version 1.0, 2011-12-25
 */
public class PathWithPaint {
    Path path;
    Paint paint;

    public PathWithPaint(Path path, Paint paint) {
        super();
        this.path = path;
        this.paint = new Paint(paint);    // (1)
        this.paint.setStyle(Paint.Style.STROKE); // 輪郭のみ    // (2)
    }

    @Override
    public String toString() {
        return String.format("[%s, %.1f, %x]", getPathString(), paint
                .getStrokeWidth(), paint.getColor());
    }

    private String getPathString() {
        return path.toString();
    }

}
---

(1)(2) あたらしいペイント・オブジェクトを生成

あたらしいペイント・オブジェクトとして保持する必要がある。そうでないと、最後に設
定したペイント・オブジェクトの値が描画時に採用されることになる。

    this.paint = new Paint(paint); // (1)

また、折れ線を描くので、ここでは、輪郭のみのスタイルにする。

    this.paint.setStyle(Paint.Style.STROKE); // 輪郭のみ    // (2)


■ レイアウト

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/redButton"
            android:onClick="onRedButtonClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/redButton" />

        <Button
            android:id="@+id/greenButton"
            android:onClick="onGreenButtonClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/greenButton" />

        <Button
            android:id="@+id/blueButton"
            android:onClick="onBlueButtonClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/blueButton" />

    </LinearLayout>

    <jp.marunomaruno.android.paintpot.CanvasView
        android:id="@+id/canvasView"
        android:layout_width="fill_parent"
        android:layout_height="300px"
        android:scaleType="fitXY"
        android:src="@drawable/kitty" />    <!-- (1) -->

    <LinearLayout
        android:id="@+id/linearLayout2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <Button
            android:id="@+id/wipeButton"
            android:onClick="onWipeButtonClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/wipeButton" />

        <Button
            android:id="@+id/heavyButton"
            android:onClick="onHeavyButtonClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/heavyButton" />

        <Button
            android:id="@+id/thinButton"
            android:onClick="onThinButtonClick"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/thinButton" />

    </LinearLayout>

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Medium Text"
        android:textAppearance="?android:attr/textAppearanceMedium" />


</LinearLayout>
---

(1) カスタムビュー

要素名に、カスタム・ビューの完全修飾名をしていするだけでよい。

    <jp.marunomaruno.android.paintpot.CanvasView
        android:id="@+id/canvasView"
        android:layout_width="fill_parent"
        android:layout_height="300px"
        android:scaleType="fitXY"
        android:src="@drawable/kitty" />    <!-- (1) -->

ここで、指定された大きさ (layout_width と layout_height) に対して、画像を自動的
にその大きさに拡大・縮小して表示する属性を指定しておきます。
        android:scaleType="fitXY"

値は以下のものが指定できる。

------------- ----------------------- -------- ----------
値            説明                    リサイズ 縦横比維持
------------- ----------------------- -------- ----------
matrix        描画時に Matrix を使用     O         X
fitXY         表示領域の大きさにする     O         X
fitStart      左上端寄せ                 O         O
fitCenter     中央寄せ                   O         O
fitEnd        右下端寄せ                 O         O
center        中央寄せ(リサイズなし)     X         -
centerCrop    中央寄せ(余白含める)       O         O
centerInside  中央寄せ(余白含めない)     O         O
------------- ----------------------- -------- ----------


■ 文字列

□ res/values/strings.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, PaintPotActivity!</string>
    <string name="app_name">PaintPot</string>
    <string name="redButton">赤</string>
    <string name="greenButton">緑</string>
    <string name="blueButton">青</string>
    <string name="wipeButton">クリア</string>
    <string name="heavyButton">太線</string>
    <string name="thinButton">細線</string>
</resources>
---