AndroidのViewやらonDrawやらCanvasやらを少し触ってみる

何の気なしに開いた解説書で、自前のViewを使って描画、みたいなのに目がとまった。
絵とかそのへんの処理は経験少なくて苦手なので、少し動かしてみて理解を進めるぞ、と。

今回作るもののイメージとしては、
レイアウト->View->Viewに乗っけたグラフィック
の順で背面から詰まれてる感じかしら。
f:id:owaraiurutorakuizu:20180907010546p:plain


▼呼び出すほう
1: レイアウトを用意。
2: ビットマップを用意。適当な画像とかで。
3: Viewのサブクラス(後述)のインスタンスを用意。コンストラクタにはビットマップを渡す。
4: レイアウトのaddView()に3のインスタンス渡すことで、レイアウトに貼り付けてやる。
みたいな感じ。
3のインスタンス.postInvalidate()
とかやるとそのたび、Viewインスタンスの描画処理部分であるonDrawを呼べる。
onDrawはViewの描画処理実体なので、一定間隔でpostInvalidate()を呼ぶことで、
アニメーション(パラパラ漫画)を実現できる。
UI(メイン)スレッドから呼ぶときはinvalidate()でいけるが、
アニメーション処理は別スレッドにするのが良い作法らしく、そのばあいpostInvalidateでないとダメだそう。


▼呼ばれるほう(View)
・Viewのサブクラスとして自作。
・中身では、描画のための諸準備とかを仕込み、onDrawメソッドをオーバーライドして描画処理を書いてやる。
・引数でもらったビットマップを、onDrawの中で、座標をちょっと変えたり少し回転して描画するような処理を書いてやれば、
 呼び出し側でpostInvalidate(つまりonDraw)を繰り返し呼ぶことで、アニメーションが表現できる。



動かしてみた。おおー、すごい。
ビットマップに、ポプ子ギュルギュル回転画像を用いたら、かわいくて楽しかったです。
回っとるやろがい!
f:id:owaraiurutorakuizu:20180907030723g:plain



MainActivity

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
import android.view.View;
import android.widget.RelativeLayout;

import java.util.Random;

/**
 * Canvasを使うプログラム(Viewのサブクラスを使う方法)
 * 画面上を、ビットマップが、回転しながら移動するアプリ。
 */
public class MainActivity extends Activity {

    static final int SAIBYOUGA_KANKAKU_MS = 20; //viewの再描画の間隔(ms)。小さいほどアニメーションが滑らかに。
    static final int SAIBYOUGA_COUNT = 5000;  //再描画回数の指定。再描画用スレッド内の処理でインクリメントがこれに達すると終了。

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

        //ベースとなるレイアウトを得ておく。
        RelativeLayout relativeLayout = findViewById(R.id.activity_main);

        //ビットマップ(=回転する絵)を得ておく。
        final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ppk); //drawableに突っ込んでおいた画像を使用。

        //viewのサブクラスを得ておく。レイアウトに乗っかる、絵コンテンツの総体、とでも捉えたらよいのかな。
        final MyView myView = new MyView(this,bitmap);

        //ベースのレイアウトにviewをaddする。乗っける、的な?
        relativeLayout.addView(myView);
        //基本は、ViewのonDraw内(描画処理の実体)を1回呼んでおしまいなので、
        //1枚絵ならば、ここまでで完成。

        //アニメの理屈は、view丸ごとの再描画を、任意の間隔で繰り返す感じ。
        //んで今回は、再描画ごとにカウンタをインクリメントして、指定回数到達で停止、のような実装。
        //再描画ごとの移動や回転、つまり動きの表現は、viewのほうに仕込んであり、それが繰り返されることでアニメーションを表現。
        //Viewの描画処理(onDraw)を繰り返し呼ぶのはスレッド作っておこなう。UI(メイン)スレッドではやらない行為、なのだそうだ。
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<SAIBYOUGA_COUNT; i++){
                    try {
                        Thread.sleep(SAIBYOUGA_KANKAKU_MS);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    myView.postInvalidate();    //←コレで結果的にViewのonDrawを呼ぶのだそう。
                    //類似のinvalidateというメソッドもあるが、別スレッドからの場合はこちらのpost~を使うとのこと。

                    myView.move();  //再描画後にコレ。次の描画用に、新しい位置座標などを更新してる。
                }
            }
        }).start();

    }


    /**
     * Viewのサブクラスを定義
     * 背景と、その上に乗っかった絵(=コンストラクタ引数で渡ってくるビットマップ)。
     * 描画処理実体はonDraw内に書かれているcanvas.~ のところ。
     * (on)Invalidateで再描画される、つまりインスタンスのonDrawが呼ばれるたび、
     * canvas.~で仕込んである回転や移動処理が実行され、結果動いて見える。
     */
    private class MyView extends View {
        private static final int STEP = 50; //これ増やすと、起動ごとの速度設定(ランダム)のふり幅が大きくなるようだ。
        final private Bitmap bitmap;
        float currentX;
        float currentY;
        float dx;
        float dy;
        int bitmapHeight;
        int bitmapWidth;
        float rotation;
        int canvasWidth = 0;
        int canvasHeight = 0;
        final private Paint mPainter = new Paint();


        /**
         * コンストラクタ
         */
        public MyView(Context context, Bitmap bitmap){
            super(context);

            //WindowManagerクラスとDisplayクラスを使って画面のサイズを取得する
            Display display = getWindowManager().getDefaultDisplay();
            Point point = new Point(0,0);
            display.getSize(point); //ここの処理で、引数(Point outSize)に渡した変数に対し、ディスプレイサイズのピクセルが代入される。
            int displayWidth = point.x; //ディスプレイ幅を取得。
            int displayHeight = point.y;    //同、高さを取得。

            //引数に取ったビットマップ(今回は乗っかってる絵)とその幅&高さを得る。
            this.bitmap = bitmap;
            bitmapHeight = bitmap.getHeight();
            bitmapWidth = bitmap.getWidth();

            //開始位置を指定。画面中央は、ディスプレイ幅の1/2と高さの1/2で得ている。
            float x0 = (float)(displayWidth/2);
            float y0 = (float)(displayHeight/2);

            //x方向、y方向の、描画ごとの移動量。変えると、アニメーションの移動速度が変化する感じか。
            //ここでは起動のたび(MyViewインスタンス生成のたび?)Randomで速度が変わっている。
            Random r = new Random();
            dx = (float)(2.0*r.nextFloat()-1.0) * STEP;
            dy = (float)(2.0*r.nextFloat()-1.0) * STEP;

            //これはよくわからない。x0,y0でも動くぽいが。
            currentX = x0 - bitmapWidth/2;
            currentY = y0 - bitmapHeight/2;

            //アンチエイリアスの設定。View内の描画物に対して機能するのかな?よくわかんない。
            mPainter.setAntiAlias(true);
        }


        /**
         * onDrawは、Viewのインスタンスが作られた(レイアウトにaddViewされた?)ときと、
         * そのインスタンスで(on)invalidateが呼ばれたときに、実行されるそうだ。
         * View上に描きたい内容は、ここで渡されるCanvas canvasを使って描く感じか。
         * @param canvas
         */
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);

            canvas.drawColor(Color.DKGRAY); //背景色を設定。


            canvasWidth = canvas.getWidth();
            canvasHeight = canvas.getHeight();

            canvas.rotate(rotation, currentX + bitmapWidth/2,currentY + bitmapHeight/2);    //回転角度は第一引数、後ろ2つの引数で回転の中心座標を決め。
            //なお後ろ2つを0,0とかにするとわかるが、rotateは、Canvasごと(viewごと)回転させる。
            //ここでは、回転の軸が常に、乗っかっているビットマップ絵の中心になるように仕込んでるので、
            //結果的に見た目としては、絵だけが回転しているように見える。

            float rotationDegree = 30;  //再描画ごとに回転する角度
            rotation += rotationDegree;
            
            //ビットマップの描画
            canvas.drawBitmap(bitmap,currentX,currentY,mPainter);
        }

        /**
         * 移動後の位置を計算
         * ディスプレイ領域から外に出ないように制御してる感じかな
         */
        protected void move(){
            if(currentX + dx < 0){
                dx = -dx;
            }
            if(currentY + dy < 0){
                dy = -dy;
            }
            if(canvasWidth < currentX + dx + bitmapWidth){
                dx  = -dx;
            }
            if(canvasHeight < currentY + dy + bitmapHeight){
                dy = -dy;
            }
            currentX += dx;
            currentY += dy;
        }
    }
}



activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:id="@+id/activity_main"
    >
    <!-- ここは特に何も書かず。 -->

</RelativeLayout>