2014년 8월 6일 수요일

[Android] SurfaceView와 Thread

1. SurfaceView는 무엇인가? 

SurfaceView를 알기 쉽게 설명하기 위해 인터넷을 찾다 보니까 아주 좋은 글과 그림이 있어 원문을 그대로 인용하겠습니다(그림은 조금 손봤습니다). 

'Android Application에서 View는 GDI Thread를 통해 Surface에 그려지게 됩니다. 만약 View에 동영상 또는 카메라 프리뷰와 같이 그려지는 양이 매우 많거나 빠른 화면 변화를 원한다면 SurfaceView를 사용해야 합니다. SurfaceView의 내용은 GDI Thread를 통해서 Surface에 그려지지 않고 다른 Thread를 통해서 그려지기 때문입니다. SurfaceView는 아래 그림과 같이 Window의 아래쪽에 위치하며, Windows를 뚫어서(Punched) 자신이 보여지게끔 합니다. 단, 해당 Window 위에 다른 View가 있는 경우 블렌딩(blended)이 되어 보여지게 됩니다.' 

 

[출처] 청하가 제안하는 소프트웨어 엔지니어로써 재미있게 사는 법  http://sozu.tistory.com/35 

이해가 가시나요? SurfaceView의 핵심적인 요소를 그림을 곁들여 아주 간결하게 설명한 글이라고 생각됩니다. 조금 더 부연 설명을 하자면, 위에서 GDI라는 말이 나오는데 Graphic Device Interface의 약자로 그래픽을 기반으로 하는 사용자 인터페이스를 의미하는 용어입니다. 요즈음은 DOS처럼 텍스트를 기반으로 하는 인터페이스는 거의 사용하지 않기 때문에 그냥 UI(User Interface)라고만 해도 그건 GDI를 의미한다고 생각하셔도 됩니다. 컴퓨터를 보던지, 폰을 보던지 모두 그래픽 환경에 아이콘을 기본으로 하고 있으니까요. 

Surface는 하나의 그래픽 버퍼로써 SurfaceView에 실제로 그림을 그리는 등의 작업을 하는 것은 SurfaceHolder라고 하는 콜백(Callback) 함수 입니다. 콜백 함수는 정의해 두기만 하면 사용자가 직접 호출하지 않더라도 운영체제가 알아서 호출해 주는 함수를 의미하는 용어입니다. 지난 강좌에 사용한 Touch나 KeyDown 이벤트 핸들러, 앞으로 만들어 갈 메뉴 등이 모두 콜백함수입니다. 

인터넷에 SurfaceView와 SurfaceHolder의 관계를 그림으로 잘 표현한 것이 있어 그것도 인용하도록 하겠습니다.
 
 

[출처] 커니의 안드로이드 이야기  http://androidhuman.tistory.com/307 

위의 그림과 같이 SurfaceHolder를 이용해서 Surface라는 버퍼에 그림을 그리면 그것이 SurfaceView에 반영이 되고 그 결과가 사용자의 View에 표시되는 방식입니다. 전문적인 용어로는 더블 버퍼링이라고 하죠. 더블 버퍼링은 이미지 등을 (처리 속도가 느린) View에 직접 그리는 것이 아니라, (처리 속도가 빠른) 메모리에서 처리한 다음 (복사하듯이) 메모리에서 View로 고속 전송을 하는 개념입니다. 


2. SurfaceView class 만들기 
이론이야 어떻든 일단 프로그램을 만들어봐야죠? 새로운 프로젝트를 시작합니다. 여기에서는 설명을 쉽게 하기 위해서 프로젝트를 
Shooting01로 하기로 합니다. 

 


새 프로젝트가 만들어지면 Package Explorer(또는 Project Explorer)에서 패키지 이름(src 바로 아래에 있는 것)을 마우스 오른쪽 버튼으로 클릭하고 [New - Class] 항목을 선택합니다. 

 


[New Java Calss] 창이 나타나면 Name란에 GameView를 입력하고 Superclass를 설정하기 위해 [Browse...] 버튼을 클릭합니다. 

 


[Superclass Selection] 창이 나타나면 [Choose a type] 란에 'sur'과 같이 몇 문자만 입력하면 이클립스는 자동 완성 기능이 있으므로 [Matching Items] 항목에 'SurfaceView'라는 항목이 나타납니다. 우리는 그것을 지정합니다. 

 

 


Supperclass가 자동으로 입력이 되었습니다. 다음에는 Interface를 만들기 위해 위의 창에서[Add...] 버튼을 클릭합니다. 마찬가지 방법으로 'sur'를 입력한 다음 SurfaceHolder를 선택합니다. 

 


SurfaceHolder가 Interfaces 항목에 추가되었습니다. 아래 그림에서 빨간색으로 표시되어 있는 .Callback은 자동으로 만들어 주지 않으므로 직접 입력해야 합니다. 

 


여기 까지는 초심자가 SurfaceView를 만드는 과정이고, SurfaceView를 만드는데 익숙해지면 위의 Superclass와 Interface를 직접 입력하는 것이 더 빠를 수 있습니다. 새로운 GameView가 만들어졌습니다. 에러 표시가 보이는군요. 빨간색 x표를 클릭하면 생성자를 지정하는 창이 나타납니다. 우리는 2번 째 항목을 선택합니다. 

 


이제 프로그램은 다음과 같이 나타납니다. 알아보기 쉽게 메소드 위에 주석을 달고, arg0, arg2 등과 같이 표시된 인수를 이해하기 쉽도록 바꾸어 두었습니다. 또, 이클립스가 만든 소스에서  surfaceCreated()와 surfaceChanged()의 위치를 서로 바꾸었습니다. 별다른 이유는 없구요, 프로그램의 흐름상 생성자 다음에 surfaceCreated()가 오는 것이 순서가 맞는 것 같아서입니다. 아, 인수와 메소드의 순서를 바꾸는 것은 우리가 프로그램을 읽기 쉽게 하는 것이지 프로그램의 성능과는 전혀 상관이 없습니다. 

 

위와 같이 메소드 위에 그 기능을 제목(Title)과 같은 형식으로 주석을 달아두면 전체의 기능이 한 눈에 들어오므로 프로그램을 쉽게 알아볼 수 있습니다. 물론 개발자는 조금 번거로울 수 있겠지요(사실은 하나 만든 다음 나머지는 죄다 복사해다 쓰는 것이니까 크게 힘들 것도 없어요). 


3. SurfaceView를 사용하기 위한 준비 

SurfaceView를 움직이는 것은 SurfaceHolder인데 우리는 아직 SurfaceHolder를 만들지 않았으므로 생성자(GameView())에서 다음과 같이 입력하여 SurfaceHolder를 만들고 콜백 함수를 등록합니다. 

 


위의 2문장은 SurfaceView를 사용하기 위해 기본적으로 필요한 것이므로 공식처럼 알고 있어야 합니다. 이것으로 SurfaceView를 직접 다루는 SurfaceHolder를 만들었습니다. 이제는 SurfaceHolder를 움직일 스레드를 만들어야 합니다. 


4. Thread 만들기 
프로그램의 적당한 위치(저는 주로 맨 끝을 사용합니다)에 다음과 같이 입력합니다. 스레드는 class로 작성합니다. 스레드 class의 기본 구조는 다음과 같습니다. class 선언부, 생성자, 실제로 반복 처리될 run() 이라는 메소드. 스레드는 자동으로 만들어 주지 않으므로 우리가 직접 입력해야 합니다. 

 


위의 스레드는 혼자서 동작하는 것은 아니고 SurfaceView에서 호출해 줘야 실행을 합니다. 스레드는 SurfaceHolder를 이용해서 그림을 그리고, 또 비트맵 이미지를 읽기 위해서는 Context가 필요할 것이므로 SurfaceView로 부터 이와 관련된 자료를 함수의 호출인자(Argument) 넘겨받던지 아니면 이들을 전역변수로 만들어 서로 공유하도록 해야 합니다. 

어떤 방법을 사용하느냐는 프로그램의 상황에 따라 다르므로 어떤 것이 더 효율적이라고 단정할 수는 없습니다. 우리의 프로젝트는 강좌가 진행됨에 따라 Touch나 Key Event에서도 SurfaceHolder를 사용할 수 있으므로 Context와 SurfaceHolder를 SurfaceView 레벨의 전역변수로 사용하는 것이 프로그램의 구성이 간편해 질 수 있습니다. 

그렇다면 SurfaceView에서 다음과 같은 전역변수를 만들어야 합니다. 인수로 넘어오는 context, holder 등과 구분하기 위해 변수명 앞에 접두어 'm'을 붙였습니다. 

 


이제 다음과 같이 Thread를 기동시킵니다. 

 


mThread.start()에 의해서 우리가 만든 스레드의 run() 메소드가 실행됩니다. 이제 할 일은 스레드에서 화면을 지지고 볶고 하는 것만 남았군요. 일단은 화면에 뭔가를 그려봅시다. 다음의 이미지를 화면에 보여주려 합니다. 

                galaxy.png 
 


일단 이미지를 읽어서 canvas.drawBitmap()으로 뿌려줘야 하므로 Bitmap이 하나 필요합니다. 그림을 그리는 것은 run()에서 처리할 것이므로 Bitmap을 스레드 레벨 전역변수로 선언하고 생성자에서 이미지를 읽어옵니다. 

 


5. 스레드로 canvas에 출력하기  

스레드로 SurfaceView에 출력하는 것은 조금 복잡합니다. 일단 코드를 보시죠. 가장 기본적인 내용만 적었습니다. 

 


위의 순환문은 종료 조건이 없으므로 무한히 반복하는 구조로 되어 있습니다. SurfaceHolder의 버퍼를 canvas에 할당하고 비트맵 이미지를 그린 다음 최종적으로(finally) 버퍼의 내용을 SurfaceView에 출력합니다. 이제 프로그램을 실행해 봅니다. 

 


에구~ Hellow World가 나타났네요, 왜 그렇죠? 그리고 보니까 우리가 SurfaceView만 만들었지 정작 메인 Activity에서 View를 설정하지 않았군요. 지난번 강좌에서는 우리가 만든 View를 변수 형식으로 호출했는데 이번 강좌부터는 View 형식으로 호출해 보도록 합니다. 물론 지난번 강좌처럼 setContentView(new GameView(..))와 같은 형식으로도 호출할 수 있습니다. 

[res/layout/main.xml]을 열고 다음과 같이 입력합니다. LinearLayout을 사용할 수도 있지만 FrameLayout이 속도면에서 다소 이득이 있으므로 FrameLayout을 사용했습니다. 


 

다시 프로그램을 실행해 봅니다. 

 


이제 이미지가 나타났습니다. 그런데 이미지가 화면보다 조금 작군요. 게임의 배경이 이렇게 한쪽으로 몰려서는 곤란하겠죠? 단말기(폰)의 해상도가 각기 다른만큼 배경 이미지는 단말기의 크기에 맞도록 스케일을 조정할 필요가 있습니다. 

지난번 강좌에서 사용했던 화면의 폭과 높이를 구하는 함수를 사용할겁니다. 프로그램을 다음과 같이 수정합니다. 

 


맨 마지막 문장을 잘 보시기 바랍니다. Bitmap.createScaledBitmap()이라는 함수를 사용하고 있지요? 원본 비트맵을 width, height만큼 늘리거나 줄여서 새로운 비트맵을 만드는 것입니다. width와 height가 화면의 크기이므로 이미지가 화면의 크기만큼 확대가 될 것입니다. 프로그램을 실행하니까 화면에 꽉찬 이미지가 나타났습니다. 

 

  
여기서 한 가지 알고 넘어가야 할 것은 위의 getSystemService로 구한 것은 Device의 폭과 높이이지 View의 폭과 높이가 아니라는 사실입니다. 무슨 말이냐 하면 getSystemService로 구한 값은 Device의 해상도가 480x800이라는 의미인데, 우리가 만든 View는 타이틀과 StatusBar가 View의 일부분을 차지하고 있으므로 순수한 View의 높이는 50~76 pixel 정도 작습니다. 물론 타이틀과 StatusBar를 없애고 전체 화면을 사용하도록 하면 같은 값이 되겠죠. 

프로그램을 만들 때 타이틀과 StatusBar를 없애지 않아야 할 경우에는 View의 높이는 50~76 정도 작게 된다는 것을 염두에 두고 좌표를 설정해야 한다는 것을 알고 있어야 합니다. 

다음은 전체 소스입니다. xml은 간단한 거라서 따로 싣지 않았습니다. 

package com.Shooting01;
import android.content.*;
import android.graphics.*;
import android.util.*;
import android.view.*;
import android.view.SurfaceHolder.Callback;
public class GameView extends SurfaceView implements Callback {     Context            mContext;     SurfaceHolder   mHolder;     GameThread     mThread;
     //-------------------------------------
     //      생성자
     //-------------------------------------
     public GameView(Context context, AttributeSet attrs) {          super(context, attrs);

          SurfaceHolder holder = getHolder();
          holder.addCallback(this);

          mHolder = holder;                             // 생성한 holder를 전역변수에 저장
          mContext = context;                        // 인수로 넘어 온 context를 전역변수에 저장 
          mThread = new GameThread();         // GameThread 생성
     }
     //-------------------------------------
     //   SurfaceView가 만들어질 때 호출됨
     //-------------------------------------
     @Override
     public void surfaceCreated(SurfaceHolder holder) {          mThread.start();                             // Thread 시작
     }
     //-------------------------------------
     //    SurfaceView의 크기가 바뀔 때 호출됨
     //-------------------------------------
     @Override
     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
     }
     //-------------------------------------
     //   SurfaceView가 종료될 때 호출됨
     //-------------------------------------
     @Override
     public void surfaceDestroyed(SurfaceHolder holder) {
     }
 
//-------------------- 여기서 부터는 스레드 영역 ----------------------------
 
      class GameThread extends Thread {           Bitmap imgBack;
   
           //-------------------------------------
           //    Thread Constructor
           //-------------------------------------
           public GameThread() {                Display display = ((WindowManager) mContext.getSystemService(mContext.WINDOW_SERVICE))
                                         .getDefaultDisplay();
                int width  = display.getWidth();                          // 화면의 폭
                int height = display.getHeight();                         // 화면의 높이

                imgBack = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.galaxy);
                imgBack = Bitmap.createScaledBitmap(imgBack, width, height, true);      // 이미지 확대
           }
 
           //-------------------------------------
           //    Thread run
           //-------------------------------------
           public void run() {                 Canvas canvas = null;                                                 // canvas를 만든다
                while (true) {
                       canvas = mHolder.lockCanvas();                           // canvas를 잠그고 버퍼 할당
                       try {
                              synchronized (mHolder) {                               // 동기화 유지
                                     canvas.drawBitmap(imgBack, 0, 0, null);    // 버퍼에 그리기
                             }
                       } finally {
                              mHolder.unlockCanvasAndPost(canvas);         // canvas의 내용을 View에 전송  
                       }
                } // while
           } // run
         
      } // End of Thread
} // End of GameView
                        


이제 SurfaceView와 스레드에 대해 어느 정도 개념이 잡히셨나요? 위에서 작성한 내용은 가장 기본적인 것으로 아직 설명하지 않는 부분이 많이 있는데,그건 다음 강좌로 넘어가야 할 것 같군요. SurfaceView가 어려운(어렵다기 보다 절차가 번거로운) 건 사실이지만 자꾸 반복해 봄으로써 완전하게 자기 것으로 만들 수 있으리라 생각합니다. 그럼 오늘 강좌는 여기서 마칩니다. 

댓글 없음:

댓글 쓰기