Sibainu Relax Room

愛犬の柴犬とともに過ごす部屋

Android Custom View に BitMap を描画

ちょっと散歩に疲れたようだ。すこし休みたいなという顔をしている柴犬です。

近くの公園にカモが集まっていました。その横でヌートリアが食事をしていました。

のどかで何かほっとする瞬間でした。いつまでもこういう光景が見られますようにと願うばかりです。

概要

カメラで撮影した画像の解像度は、デフォルトだとデバイス自体の解像度で私の所有しているタブレットだと 2592×1944 あります。

ところが画面の解像度は 1920×1200 でボタンなどを配置すると画像表示エリアはもっと小さくなります。

したがって、Activity を開いたときは写真画像の一部(私の場合 30%)が表示されることになります。

これでは不便なので、ビットマップ画像は最大の大きさで縮小することにします。

この件の前に Canvas.drawBitmap を勉強しておいてよかったです。やはりリファレンスを読むことが大事と改めて感じました。記録することにします。

WEBのみでは断片的で覚えにくいので最初に購入した Kotlin の本です。

GestureView.java の修正

変数の追加

初期時の Matrix を作成できる変数を作ります。

    //デフォルトの画像の縮尺
    private float firstrate = 1.0f;

    //描画の左上のx座標
    private float firstx =0.0f;

    //描画の左上のy座標
    private float firsty =0.0f;

    //View自身の横幅
    private int viewwidth;

    //View自身の高さ
    private int viewheight;

縮小率の計算

画像縮小率および描画時の左と上の余白を計算して、初期の Matrix を作成します。

        // BitMap の全体表示をデフォルトとする drawmatirix を作成します

        //キャンバスの大きさを取得します
        viewwidth = getWidth();   // Viewの横幅を取得
        viewheight = getHeight(); // Viewの高さを取得

        //横幅の縮小率を計算します
        int bmwidth = _bm.getWidth();
        float widthrate = viewwidth * 1.0f / bmwidth;

        //変数作成時より小さければ入れ替えます
        if (firstrate > widthrate) {
            firstrate = widthrate;
        }

        //高さの縮小率を計算します
        int bmheight = _bm.getHeight();
        float heightrate = viewheight * 1.0f / bmheight;

        //これまでの値より小さければ入れ替えます
        if (firstrate > heightrate) {
            firstrate = heightrate;
        }

        //変数作成時と違っていれば drawmatrix を更新します 
        if (firstrate != 1.0f) {
            //キャンバスの中心に描画するために左と上の余白を計算します
            firstx = (viewwidth - bmwidth * firstrate) / 2;
            firsty = (viewheight - bmheight * firstrate) / 2;

            //相似の拡大縮小移動 sx:firstrate  sy:firstrate  px:firstx  py:firsty
            //方法1
            drawmatrix.postScale(firstrate, firstrate, firstx, firsty);

            //方法2
            drawmatrix.postScale(firstrate, firstrate);
            drawmatrix.postTranslate(firstx, firsty);
        }

上のスマホのスクリーンショットは、方法1による縮小・移動です。左の余白が変です。キャンバスの中央にしたつもりがそうなってないです。左余白が計算の半分くらいしかありませんので、余白も縮尺率で小さくなったようです。

下のスマホのスクリーンショットは、方法2による縮小・移動です。キャンバスの中央になっています。

方法1による縮小・移動は注意した方がよさそうです。

記述場所

どのタイミングで先の計算ができるかが一番問題になるところです。Viewの横幅・高さ、ビットマップ画像の横幅・高さを取得でき、さらに描画が実行する前であることが必要です。

私は、クラスの onLayout 時が最善と判断してそこに書きました。


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        //ここで計算するようにしました

    }

onDoubleTapの修正

onDoubleTap は画面遷移した状態に戻すわけですから、ここにもコードを追加して書きます。

        @Override
        public boolean onDoubleTap (MotionEvent e) {
            Log.d("doubletap","onDoubleTap");
            drawmatrix.reset();

            //追加したコード
            //drawmatrix.postScale(firstrate, firstrate, firstx, firsty);
            drawmatrix.postScale(firstrate, firstrate);
            drawmatrix.postTranslate(firstx, firsty);

            invalidate();
            return super.onDoubleTap(e);
        }

全体コード

copy

package org.sibainu.relax.room.scalegesturedetector;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;

import androidx.annotation.Nullable;

public class GestureView extends View {

    private Context context;
    //表示するビットマップ画像
    private Bitmap _bm;
    //ピンチ操作開始のX座標
    private float _focusx;
    //ピンチ操作開始のY座標
    private float _focusy;
    //ピンチ操作時の画像の縮尺
    private float _lastscalefactor = 1.0f;
    //デフォルトの画像の縮尺
    private float firstrate = 1.0f;
    //描画の左上のx座標
    private float firstx =0.0f;
    //描画の左上のy座標
    private float firsty =0.0f;
    //View自身の横幅
    private int viewwidth;
    //View自身の高さ
    private int viewheight;

    //画像の描画を設定するオブジェクト
    private Matrix drawmatrix = new Matrix();
    //Paintオブジェクト
    private Paint paint = new Paint();

    //ピンチ操作時の動き感知するリスナーの作成
    private ScaleGestureDetector scalegesturedetector;
    private ScaleGestureDetector.SimpleOnScaleGestureListener scalegesturelistener =
            new ScaleGestureDetector.SimpleOnScaleGestureListener() {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            Log.d("scale","onscalebegin");
            //ピンチ操作開始
            //開始座標を取得
            _focusx = detector.getFocusX();
            _focusy = detector.getFocusY();
            return super.onScaleBegin(detector);
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            //ピンチ操作終了
            super.onScaleEnd(detector);
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            //ピンチ操作中
            //縮尺率を取得
            _lastscalefactor = detector.getScaleFactor();
            //縦横の拡大縮小を設定するマトリックスの作成
            drawmatrix.postScale(_lastscalefactor,
                                 _lastscalefactor,
                                 _focusx,
                                 _focusy);
            //これを実行することにより onDraw が発火
            invalidate();
            super.onScale(detector);
            return true;
        }
    };

    //2本指スクロールの動き感知するリスナーの作成
    private GestureDetector gesturedetector;
    private GestureDetector.SimpleOnGestureListener gesturelistener =
            new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll (MotionEvent e1,
                                 MotionEvent e2,
                                 float distanceX,
                                 float distanceY) {
            Log.d("scroll","onscroll");
            // スクロールしたとき
            if (e1.getPointerId(0) ==
                    e2.getPointerId(0)) {
                // 画像を移動
                drawmatrix.postTranslate(-distanceX, -distanceY);
                //これを実行することにより onDraw が発火
                invalidate();
            }
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        @Override
        public boolean onDoubleTap (MotionEvent e) {
            Log.d("doubletap","onDoubleTap");
            drawmatrix.reset();
            //drawmatrix.postScale(firstrate, firstrate, firstx, firsty);
            drawmatrix.postScale(firstrate, firstrate);
            drawmatrix.postTranslate(firstx, firsty);
            invalidate();
            return super.onDoubleTap(e);
        }
    };

    // コンストラクタ
    public GestureView (Context context) {
        super(context);
        this.context = context;
        init();
    }

    public GestureView (Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
    }

    public GestureView (Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
    }

    private void init () {
        //リスナーを組み込む
        scalegesturedetector = new ScaleGestureDetector(context, scalegesturelistener);
        gesturedetector = new GestureDetector(context, gesturelistener);
    }

    @Override
    public boolean onTouchEvent (MotionEvent event) {
        super.onTouchEvent(event);
        // すべてのGestureはここを通る
        Log.d("touchevent",String.valueOf(gesturedetector.onTouchEvent(event)));
        return scalegesturedetector.onTouchEvent(event) ||
               gesturedetector.onTouchEvent(event) ||
               super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //再描画時の累積した Matrix drawmatrix を適応
        canvas.save();
        canvas.drawBitmap(_bm, drawmatrix, paint);
        canvas.restore();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        // BitMap の全体表示をデフォルトとする drawmatirix を作成します
        viewwidth = getWidth();   // Viewの横幅を取得
        viewheight = getHeight(); // Viewの高さを取得

        int bmwidth = _bm.getWidth();
        float widthrate = viewwidth * 1.0f / bmwidth;
        if (firstrate > widthrate) {
            firstrate = widthrate;
        }

        int bmheight = _bm.getHeight();
        float heightrate = viewheight * 1.0f / bmheight;
        if (firstrate > heightrate) {
            firstrate = heightrate;
        }
        if (firstrate != 1.0f) {
            //キャンバスの中心に描画します
            firstx = (viewwidth - bmwidth * firstrate) / 2;
            firsty = (viewheight - bmheight * firstrate) / 2;
            //相似の拡大縮小・移動 sx:firstrate  sy:firstrate  px:firstx  py:firsty
            //drawmatrix.postScale(firstrate, firstrate, firstx, firsty);
            drawmatrix.postScale(firstrate, firstrate);
            drawmatrix.postTranslate(firstx, firsty);
        }
    }

    public void setImageBitmap(Bitmap bm) {
        _bm = bm;
        invalidate();
    }
}

今回はここまでとします。