まずはSurfaceView

何をblogネタにしようかと考えていて、思いついたのがSurfaceViewでした。ネットに転がっている情報をもとに以前もSurfaceViewを使ったCodeを書いてみたのですが、何となく久しぶりに復習をしようと思います。

今回は、Iconを上下左右に移動させながら描画するというものを作りました。

SurfaceView実装の要点を下記にまとめます。
  1. SurfaceViewを拡張したclassを作成
  2. SurfaceHolderからのCallbackを受け取るSurfaceHolder.Callbackを実装(上記classに実装)
  3. call back methodの実装
  4. 描画Threadを実装。Threadのrun()の中で、SurfaceHolder classのlockCanvas() methodを呼び出し、canvasを取得
  5. 取得したcanvasで描画処理
  6. unlockCanvasAndPost() methodでcanvasを解放
それと実装してみての気づきを下記にまとめます。
  • SurfaceHolder classのcall back methodはUI threadから呼ばれる(当たり前かな...)
  • 描画するたび、canvasの塗りつぶしをしないと前の画像が残ったままになる
  • Drawable classのdraw(Canvas)を呼ぶ前に必ずsetBounds()を呼ぶ必要がある。呼ばないと描画されませんでした。
で、実装はこんな感じになりました。
//SurfaceViewを拡張したclassを作成
//SurfaceHolderからのCallbackを受け取るSurfaceHolder.Callbackを実装(上記classに実装)
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback{
    
    private static final String TAG = "MySurfaceView";
    
    private static final String THREAD_NAME = "DrawingThread";
    private MyDrawingThread mDrawingThread;
    private Context mContext;
    
    public MySurfaceView(Context context) {
        super(context);
        mContext = context;
        getHolder().addCallback(this);
        mDrawingThread = new MyDrawingThread(THREAD_NAME, this, mContext);
    }
    
    public MySurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        getHolder().addCallback(this);
        mDrawingThread = new MyDrawingThread(THREAD_NAME, this, mContext);
    }
    
    //call back methodの実装
    //SurfaceHolder classのcall back methodはUI threadから呼ばれる
    public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
    }
    
    //call back methodの実装
    //SurfaceHolder classのcall back methodはUI threadから呼ばれる
    public void surfaceCreated(SurfaceHolder holder) {
        Log.d(TAG, "Thread: " + Thread.currentThread().getName() + " call surfaceCreated()");
        startDrawing();
    }

    //call back methodの実装
    //SurfaceHolder classのcall back methodはUI threadから呼ばれる
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d(TAG, "Thread: " + Thread.currentThread().getName() + " call surfaceDestroyed()");
        stopDrawing();
    }
    
    private void startDrawing(){
        mDrawingThread.startDrawing();
    }
    
    private void stopDrawing(){
        mDrawingThread.stopDrawing();
        try{
            mDrawingThread.join();
        }catch(InterruptedException e){
        }
    }
    
    private class MyDrawingThread extends Thread{
        
        private static final int SPEED_X = 5;
        private static final int SPEED_Y = 5;
        
        private static final int LEFT = -1;
        private static final int RIGHT = 1;
        private static final int UP = -1;
        private static final int DOWN = 1;
        
        private volatile boolean mRun = false;
        
        private SurfaceView mSurfaceView;
        private Drawable mTargetImage;
        
        private int mSurfaceWidth;
        private int mSurfaceHeight;
        private Rect mBoundary = new Rect();
        private int mTargetImageWidth;
        private int mTargetImageHeight;
        private int mDirectionX = RIGHT;
        private int mDirectionY = DOWN;
        
        private Paint mPaint = new Paint();
        
        public MyDrawingThread(String name, SurfaceView surfaceView, Context context){
            super(name);
            mSurfaceView = surfaceView;
            mTargetImage = context.getResources().getDrawable(R.drawable.icon);
        }
        
        private void initUiEnv(SurfaceView surfaceView){
            mSurfaceWidth = surfaceView.getWidth();
            mSurfaceHeight = surfaceView.getHeight();
            
            mTargetImageWidth = mTargetImage.getIntrinsicWidth();
            mTargetImageHeight = mTargetImage.getIntrinsicHeight();
            
            mBoundary.left = 0;
            mBoundary.top = 0;
            mBoundary.right = mTargetImageWidth;
            mBoundary.bottom = mTargetImageHeight;
            
            mPaint.setColor(Color.WHITE);
        }
        
        public void startDrawing(){
            mRun = true;
            initUiEnv(mSurfaceView);
            this.start();
        }
        
        public void stopDrawing(){
            mRun = false;
        }
        
        //描画Threadを実装
        @Override
        public void run() {
            while(mRun){
                //SurfaceHolder classのlockCanvas() methodを呼び出し、canvasを取得
                Canvas canvas = mSurfaceView.getHolder().lockCanvas();

                //取得したcanvasで描画処理
                doDraw(canvas);

                //unlockCanvasAndPost() methodでcanvasを解放
                mSurfaceView.getHolder().unlockCanvasAndPost(canvas);
            }
        }
        
        private void doDraw(Canvas canvas){
      //描画するたび、canvasの塗りつぶしをしないと前の画像が残ったままになる
            canvas.drawColor(Color.BLACK);

            updateBoundary();

            //Drawable classのdraw(Canvas)を呼ぶ前に必ずsetBoundary()を呼ぶ必要がある。
            mTargetImage.setBounds(mBoundary);
            mTargetImage.draw(canvas);
            canvas.drawText("Left:" + mBoundary.left + " Right:" + mBoundary.right + 
          " Top:" + mBoundary.top + " Bottom:" + mBoundary.bottom,
                     0, mSurfaceHeight - 20, mPaint);
        }
        
        private void updateBoundary(){
            int newLeft = 0;
            int newTop = 0;
            
            newLeft = mBoundary.left + (SPEED_X * mDirectionX);
            
            if(mDirectionX == LEFT && newLeft <= 0){
                mDirectionX = RIGHT;
                mBoundary.left = 0;
                mBoundary.right = mTargetImageWidth;
            }else if(mDirectionX == RIGHT && (newLeft + mTargetImageWidth) >= mSurfaceWidth){
                mDirectionX = LEFT;
                mBoundary.right = mSurfaceWidth;
                mBoundary.left = mBoundary.right - mTargetImageWidth;
            }else{
                mBoundary.left = newLeft;
                mBoundary.right = mBoundary.left + mTargetImageWidth;
            }
            
            newTop = mBoundary.top + (SPEED_Y * mDirectionY);
            
            if(mDirectionY == UP && newTop <= 0){
                mDirectionY = DOWN;
                mBoundary.top = 0;
                mBoundary.bottom = mTargetImageHeight;
            }else if(mDirectionY == DOWN && (newTop + mTargetImageHeight) >= mSurfaceHeight){
                mDirectionY = UP;
                mBoundary.bottom = mSurfaceHeight;
                mBoundary.top = mBoundary.bottom - mTargetImageHeight;
            }else{
                mBoundary.top = newTop;
                mBoundary.bottom = mBoundary.top + mTargetImageHeight;
            }
        }
    }
}

こんな感じでどうでしょう。始めてのpostだったので、まだ、未熟なとこがありますが、ご了承を。

HashTag #android, #java