본문 바로가기

IT/Android

SurfaceView

서페이스 뷰( SurfaceView )

일반 뷰는 캔버스에 그리기를 수행한다. 모든 그리기는 자동으로 더블 버퍼링되므로 깜박거림은 전혀 없다. 그러나 메인 스레드에서 그려야 하므로 속도가 빠르지 못하며 그리기를 하는 동안에는 사용자의 입력을 받을 수 없으므로 반응성이 좋지 못하다. 게임처럼 화면 전환 속도가 빠르다거나 지도처럼 그리기를 위한 연산이 복잡한 프로그램은 OnDraw 실행하는 동안 스레드가 잠시 멈춘 것처럼 보이며 일시적으로 입력에 반응하지 못한다.

그렇다고 해서 그리기 동작을 스레드로 분리할 수도 없는데 왜냐하면 메인이 아닌 스레드는 뷰나 캔버스를 직접적으로 건드리지 못하기 때문이다. 이런 여러 가지 문제점들을 해결하기 위한 장치가 바로 서피스 뷰이다.

 

일반적인 onDraw방법

import java.util.Random;

import android.graphics.*;

 

class Ball {

    int x, y;

    int rad;

    int dx, dy;

    int color;

    int count;

 

    static Ball Create(int x, int y, int Rad) {

        Random Rnd = new Random();

        Ball NewBall = new Ball();

 

        NewBall.x = x;

        NewBall.y = y;

        NewBall.rad = Rad;

 

        do {

            NewBall.dx = Rnd.nextInt(11) - 5;

            NewBall.dy = Rnd.nextInt(11) - 5;

        } while (NewBall.dx == 0 || NewBall.dy == 0);

        NewBall.count = 0;

        NewBall.color = Color.rgb(Rnd.nextInt(256), Rnd.nextInt(256),

                Rnd.nextInt(256));

 

        return NewBall;

    }

 

    void Move(int Width, int Height) {

        x += dx;

        y += dy;

 

        if (x < rad || x > Width - rad) {

            dx *= -1;

            count++;

        }

        if (y < rad || y > Height - rad) {

            dy *= -1;

            count++;

        }

    }

 

    // 그리기

    void Draw(Canvas canvas) {

        Paint pnt = new Paint();

        pnt.setAntiAlias(true);

 

        int r;

        int alpha;

 

        for (r = rad, alpha = 1; r > 4; r--, alpha += 5) {

            pnt.setColor(Color.argb(alpha, Color.red(color),

                    Color.green(color), Color.blue(color)));

            canvas.drawCircle(x, y, r, pnt);

        }

    }

}

 

MyView.java

import android.content.*;

import android.view.View;

import android.graphics.*;

import android.os.Handler;

import android.os.Message;

import android.view.MotionEvent;

import java.util.ArrayList;

 

public class MyView extends View {

    Bitmap mBack;

    ArrayList<Ball> arBall = new ArrayList<Ball>();

    final static int DELAY = 50;

    final static int RAD = 24;

 

    public MyView(Context context) {

        super(context);

        mBack = BitmapFactory.decodeResource(context.getResources(),

                R.drawable.activity);

        mHandler.sendEmptyMessageDelayed(0, DELAY);

    }

 

    public boolean onTouchEvent(MotionEvent event) {

        if (event.getAction() == MotionEvent.ACTION_DOWN) {

            Ball NewBall = Ball.Create((int) event.getX(), (int) event.getY(),

                    RAD);

            arBall.add(NewBall);

            invalidate();

            return true;

        }

        return false;

    }

 

    public void onDraw(Canvas canvas) {

        canvas.drawBitmap(mBack, 0, 0, null);

        for (int idx = 0; idx < arBall.size(); idx++) {

            arBall.get(idx).Draw(canvas);

        }

    }

 

    Handler mHandler = new Handler() {

        public void handleMessage(Message msg) {

            Ball B;

            for (int idx = 0; idx < arBall.size(); idx++) {

                B = arBall.get(idx);

                B.Move(getWidth(), getHeight());

                if (B.count > 4) {

                    arBall.remove(idx);

                    idx--;

                }

            }

            invalidate();

            mHandler.sendEmptyMessageDelayed(0, DELAY);

        }

    };

}

 

 

void surfaceCreated( SurfaceHoler holder )

    표면이 처음 생성된 직후에 호출된다. 이 메서드가 호출된 이후부터 표면에 그리기가 허용된다. 단, 표면에는 한 스레드만 그리기를 수행할 수 있다. 메인 스레드가 이 콜백을 구현했다면 이 메서드를 받았을 때 그리기 스레드를 생성해야 한다.

void surfaceDestroyed( SurfaceHolder holder )

    표면이 파괴되기 직전에 호출된다. 이 메서드가 리턴된 후부터는 표면이 유효하지 않으므로 더 이상 그리기를 해서는 안 된다. 스레드에게 그리기를 즉시 종료하도록 신호를 보내야 한다.

void surfaceChanged( SurfaceHolder holder, int format, int width, int height )

    표면의 색상이나 포맷이 변경되었을 때 호출된다. 최소한 한번은 호출되므로 이 메서드로 전달된 인수를 통해 표면의 크기를 초기화 하면 된다.

class MyView extends SurfaceView implements SurfacehHolder.Callback

    표면을 관리하는 주체는 SurfaceHolder 객체이다. 이 객체를 통해 표면의 크기나 색상 등을 관리하며 표면으로 출력을 내 보낼 수도 있다. SurfaceView 파생 뷰는 다음 메서드로 홀드를 구한다.

SurfaceHolder SurfaceView.GetHolder()

    홀더를 구한 후 제일 먼저 할 일은 표면의 변화를 통지받기 위한 콜백 객체를 등록하는 것이다. 다음 메서드로 콜백을 등록한다. 콜백 객체를 등록해 놓으면 시스템은 표면의 변화가 발생할 때마다 콜백의 메서드를 호출한다.

void Surfaceholder.addCallback( SurfaceHolder.Callback callback )

    Surfaceholder.Callback 구현 객체를 인수로 전달하되 대개의 경우 뷰 자신이 콜백 객체를 겸하므로 addCallback(this)로 호출하면 된다. 스레드에서 그리기를 수행할 때는 다음 두 메서드를 사용한다.

Canvas Surfaceholder.lockCanvas()

void SerfaceHolder.unlockCanvasAndPost( Canvas canvas )

lockCanvas는 표면을 잠그고 표면에 대한 캔버스를 제공한다. 이후 뷰에 그리듯이 모든 출력 메서드를 다 사용할 수 있으며 이때 출력은 화면으로 바로 나가지 않고 표면의 비트맵에 그려진다. 이전에 표면에 그려 놓은 그림은 따로 저장되지 않으므로 모든 픽셀을 다 채운 후 다시 그려야 한다. unlockCanvasAndPost 메서드는 표면 비트맵에 그려진 그림을 화면으로 내 보내며 비로소 사용자 눈에 출력된 내용이 보이게 된다.

스레드는 이 두 메서드 호출문 사이에서 그리기를 수행한다. SurfaceView 단독으로 그리기를 하는 것이 아니라 인터페이스까지 복잡하게 얽혀 있는데다 제대로 사용하려면 항상 스레드를 만들어야 하므로 얼른 감이 오지 않을 것이다. 서피스 뷰로 다시 만들어 보자.

 

서피스 뷰일 경우

import android.content.*;

import android.view.SurfaceView;

import android.view.SurfaceHolder;

import android.graphics.*;

import android.view.MotionEvent;

import java.util.ArrayList;

 

public class SurfView extends SurfaceView implements SurfaceHolder.Callback {

    Bitmap mBack;

    ArrayList<Ball> arBall = new ArrayList<Ball>();

    final static int DELAY = 50;

    final static int RAD = 24;

    SurfaceHolder mHolder;

    DrawThread mThread;

 

    public SurfView(Context context) {

        super(context);

        mBack = BitmapFactory.decodeResource(context.getResources(),

                R.drawable.activity);

 

        // 표면에 변화가 생길 때의 이벤트를 처리할 콜백을 자신으로 지정한다.

        mHolder = getHolder();

        mHolder.addCallback(this);

    }

 

    class DrawThread extends Thread {

        boolean bExit;

        int mWidth, mHeight;

        SurfaceHolder mHolder;

 

        public DrawThread(SurfaceHolder Holder) {

            mHolder = Holder;

            bExit = false;

        }

 

        public void SizeChange(int Width, int Height) {

            mWidth = Width;

            mHeight = Height;

        }

 

        public void run() {

            Canvas canvas;

            Ball B;

 

            while (bExit == false) {

                // 애니메이션 진행

                for (int idx = 0; idx < arBall.size(); idx++) {

                    B = arBall.get(idx);

                    B.Move(mWidth, mHeight);

                    if (B.count > 4) {

                        arBall.remove(idx);

                        idx--;

                    }

                }

 

                // 그리기

                synchronized (mHolder) {

                    canvas = mHolder.lockCanvas();

                    if (canvas == null)

                        break;

                    canvas.drawColor(Color.BLACK);

                    canvas.drawBitmap(mBack, 0, 0, null);

                    Paint paint = new Paint();

                    paint.setColor(Color.WHITE);

                    canvas.drawText("메롱", 100, 100, paint);

 

                    for (int idx = 0; idx < arBall.size(); idx++) {

                        arBall.get(idx).Draw(canvas);

                        if (bExit)

                            break;

                    }

                    mHolder.unlockCanvasAndPost(canvas);

                }

                try {

                    Thread.sleep(MyView.DELAY);

                } catch (Exception e) {

                }

 

            }

        }

    }

 

    // 표면이 생성될 때 그리기 스레드를 시작한다.

    public void surfaceCreated(SurfaceHolder holder) {

        mThread = new DrawThread(mHolder);

        mThread.start();

    }

 

    // 표면의 크기가 바뀔 때 크기를 기록해 놓는다.

    public void surfaceChanged(SurfaceHolder holder, int format, int width,

            int height) {

        if (mThread != null) {

            mThread.SizeChange(width, height);

        }

    }

 

    // 표면이 파괴될 때 그리기를 중지한다.

    public void surfaceDestroyed(SurfaceHolder holder) {

        mThread.bExit = true;

        for (;;) {

            try {

                mThread.join();

                break;

            } catch (Exception e) {

            }

        }

    }

 

    public boolean onTouchEvent(MotionEvent event) {

        if (event.getAction() == MotionEvent.ACTION_DOWN) {

            synchronized (mHolder) {

                Ball NewBall = Ball.Create((int) event.getX(),

                        (int) event.getY(), RAD);

                arBall.add(NewBall);

            }

            return true;

        }

        return false;

    }

}

 

책에서는 속도차이가 훨씬 더 빠르다고 하는데 공 100개 이상 그렸을 경우 별 차이를 못느끼겠다.; 아무튼 Thread로 돌리기 때문에 CPU점유율이 낮고 메인 스레드는 터치 입력을 즉시 처리할 수 있어서 반응성이 좋아진다는 점이다. 이 외에도 서피스 뷰에서 오버레이 처리가 쉽고 일반 뷰보다 Z순서가 항상 더 아래쪽이어서 서피스 뷰 위쪽에 다른 뷰들을 겹쳐 놓아도 가려지지 않고 잘 보인다. 뿐만 아니라 알파 블랜딩 까지도 훌륭하게 처리하므로 위쪽의 뷰가 반투명 해도 부드럽게 처리 된다. 이 기능을 잘 활용하면 서피스 뷰에 동영상을 출력하면서 위쪽에 자막 뷰를 둔다거나 지도 위에 세부 정보를 보기 좋게 출력 할 수 있다.


'IT > Android' 카테고리의 다른 글

센서 관련 블로그들  (0) 2012.01.26
안드로이드 센서 사용하기  (0) 2012.01.26
자바 안드로이드 시간 구하는데 유용한 예제 코드  (0) 2012.01.25
Handler  (0) 2012.01.10
Android 코딩 시작하기!  (0) 2011.12.22