반응형

퍼온글입니다.

이번엔 간단히 (정말 정말 제일 간단한것 맞습니다)

카메라를 건드려 볼까합니다. 

(소스는 대부분 dev.android.com 에서 예제 퍼온거 조금 수정한 정도입니다. 영어 되시는 분들은 그냥 안드로이드 홈페이지에서 직접 읽어보시는게 더 이해가 빠를 수 있습니다)

우선 안드로이드 카메라는 몇가지 어이 없으면서 그러려니 해야하는 기본 원칙이 있습니다. (하나도 중요하진 않습니다 패스 하셔도 됩니다)

 

1. 카메라는 미리보기 화면을 가져야한다.

2. 카메라는 가로로 들고 찍는다.

 

어처구니가 없습니다.

일단 우리는 몰래카메라를 찍고 싶을 수 도 있고, 세로로 들고 셀카를 찍고 싶을 수 도 있는데 말이죠.

 

위의 두가지가 "기본" 일뿐이지 "안된다" 고는 안했습니다. 미리보기를 만들어 놓고 투명하게 만들어 버릴 수도, 1*1 사이즈로 숨겨 버릴 수도 있죠. 가로만 된다고요? 아뇨, 기본으로 가로모드라고 가정한다고요. 화면을 돌리거나 주변 UI만 세로로 돌려 버려도 됩니다. 그것도 아니라면 사진을 찍어놓고 데이터를 세로로 변환해도 되겠죠.

 

어쨌든 안드로이드 카메라 코딩을 하다보면 왜 이러지? 싶은 부분이 생긴다면, 그냥 저 위의 두가지 기본 원칙이 그러하니 그렇구나 하고 넘어가자는 말입니다.

 

 

서론이 길어지는군요.

 

어플에서 사진을 찍는 방법은 두가지가 있습니다. 

 

1. 다른 카메라앱을 호출해서 찍고, 사진을 리턴받기.

2. 직접 사진찍고 처리하기.

 

저는 2번으로 가보겠습니다. 1번은 재미 없어 보이잖아요.

 

우선 전체 절차는 크게 다음과 같습니다.

 

1. 카메라 하드웨어 유무 확인및, 액세스 가능한지 확인.

2. 프리뷰 클래스 생성

3. 촬영 시작용 리스너 생성

4. 캡쳐 후 처리

5. 카메라 리소스 반환

 

네, 다른 방법도 있고, 생략 가능한 편법들이 존재 하지만 우린 기본기를 배우는 중이니까 위의 5단계는 모두 중요합니다. 그러니까 빨간색.

 

1. 카메라 확인

 

카메라 리소스는 기기 전체에 한번에 한개만 할당 가능하기 때문에 다른 어플리케이션에서 사용중이거나, 사용후 반환하지 않은 경우에는 다른 앱에서 카메라를 사용 할 수 없게 될 뿐만 아니라 runtime exception을 발생시켜 버리므로 주의해야합니다.

 

 


 private boolean checkCameraHardware(Context context) {
     if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
         // 카메라가 최소한 한개 있는 경우 처리
      Log.i(TAG, "Number of available camera : "+Camera.getNumberOfCameras());
         return true;
     } else {
         // 카메라가 전혀 없는 경우 
      Toast.makeText(mContext, "No camera found!", Toast.LENGTH_SHORT).show();
         return false;
     }
 }
 

 

사용가능 여부를 확인했으니 이제 카메라 인스턴스를 하나 호출합니다.

 

 

 public static Camera getCameraInstance(){
     Camera c = null;
     try {
         c = Camera.open(); 
     }
     catch (Exception e){
         // 사용중이거나 사용 불가능 한 경우
     }
     return c;
 }

 

 

open() 의 매개변수로 int 값을 받을 수 도 있는데, 일반적으로 0이 후면 카메라, 1이 전면 카메라를 의미합니다. 

보다 명확히 하자면, 카메라 id  값의 범위는 0 부터 Camera.getNumberOfCameras()-1  까지 인데, 대체로 Camera.getNumberOfCameras() 가 2 입니다. (폰의 경우 대게 전후 각각 한개씩 카메라가 있으므로...)

 

2. 프리뷰 클래스 생성

 

카메라 프리뷰 클래스는 SurfaceView 클래스를 상속받아서 뷰 레이아웃안에 들어갈 대상 클래스입니다. 프리뷰가 생성/파괴시, 혹은 해상도 변경, 화면 회전등에 대한 프리뷰 변경을 위한 콜백 인터페이스를 구현해야하므로 SurfaceHolder.Callback을 implement 합니다.

 

 



각 대상들의 관계는 위 도표와 같이 이해 할 수 있습니다.


일단 기본적인 내용으로만 구성한 프리뷰 클래스의 소스는 다음과 같습니다.


 

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
 String TAG = "CAMERA";
    private SurfaceHolder mHolder;
    private Camera mCamera;

    public CameraPreview(Context context, Camera camera) {
        super(context);
        mCamera = camera;

        // SurfaceHolder 가 가지고 있는 하위 Surface가 파괴되거나 업데이트 될경우 받을 콜백을 세팅한다 
        mHolder = getHolder();
        mHolder.addCallback(this);
        // deprecated 되었지만 3.0 이하 버젼에서 필수 메소드라서 호출해둠.
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }

    public void surfaceCreated(SurfaceHolder holder) {
        // Surface가 생성되었으니 프리뷰를 어디에 띄울지 지정해준다. (holder 로 받은 SurfaceHolder에 뿌려준다. 
        try {
   Camera.Parameters parameters = mCamera.getParameters();
   if (getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
    parameters.set("orientation", "portrait");
    mCamera.setDisplayOrientation(90);
    parameters.setRotation(90);
   } else {
    parameters.set("orientation", "landscape");
    mCamera.setDisplayOrientation(0);
    parameters.setRotation(0);
   }
   mCamera.setParameters(parameters);

   mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.d(TAG, "Error setting camera preview: " + e.getMessage());
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // 프리뷰 제거시 카메라 사용도 끝났다고 간주하여 리소스를 전부 반환한다
     if (mCamera != null) {
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }

    private Camera.Size getBestPreviewSize(int width, int height)
    {
            Camera.Size result=null;    
            Camera.Parameters p = mCamera.getParameters();
            for (Camera.Size size : p.getSupportedPreviewSizes()) {
                if (size.width<=width && size.height<=height) {
                    if (result==null) {
                        result=size;
                    } else {
                        int resultArea=result.width*result.height;
                        int newArea=size.width*size.height;

                        if (newArea>resultArea) {
                            result=size;
                        }
                    }
                }
            }
        return result;

    }
    
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        // 프리뷰를 회전시키거나 변경시 처리를 여기서 해준다.
        // 프리뷰 변경시에는 먼저 프리뷰를 멈춘다음 변경해야한다.

        if (mHolder.getSurface() == null){
          // 프리뷰가 존재하지 않을때
          return;
        }

        // 우선 멈춘다 
        try {
            mCamera.stopPreview();
        } catch (Exception e){
         // 프리뷰가 존재조차 하지 않는 경우다 
        }

        
        // 프리뷰 변경, 처리 등을 여기서 해준다.
<pre>        Camera.Parameters parameters = mCamera.getParameters();
        Camera.Size size = getBestPreviewSize(w, h);
        parameters.setPreviewSize(size.width, size.height);
        mCamera.setParameters(parameters);</pre>
        // 새로 변경된 설정으로 프리뷰를 재생성한다 
        try {
            mCamera.setPreviewDisplay(mHolder);
            mCamera.startPreview();

        } catch (Exception e){
            Log.d(TAG, "Error starting camera preview: " + e.getMessage());
        }
    }
}

여기서 getBestPreviewSize() 메소드는 꼭 저처럼 짤 필요는 없고, 유저가 선택 하도록 만들어도 되는데, 일단 가장 넓이(가로*세로)가 가장 큰 해상도가 고해상도 프리뷰라고 간주해서 가장 고해상도의 프리뷰 사이즈를 반환토록 만들었습니다.

그 다음에는 만든 프리뷰 클래스를 메인 레이어에 삽입해줍니다. 프리뷰 위에 다른 버튼등의 UI 컴포넌트들을 배치 시킬수 있도록 RelativeLayout을 사용했고, 프리뷰는 FrameLayout 으로 불러오게 만들어둡니다.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
  <FrameLayout
    android:id="@+id/camera_preview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    />

  <Button
      android:id="@+id/button_capture"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentRight="true"
      android:layout_centerVertical = "true"
      android:text="Capture" />

</RelativeLayout>

 대체로 카메라는 기본적으로 가로모드를 정상 모드로 간주하기때문에 여기 예제에서도 일단 가로모드 UI로 만들어 두지만, 실제 카메라를 만들경우에는 기기의 상태에 따라 가로 세로 UI가 변하도록 만들어 줘야합니다.

 

 

 

3. 메인 액티비티 마저 마무리

 

메인 액티비티에서는 아까 위에서 미리 짜둔 메소드들과 만들어둔 레이아웃을 뷰로 가져오고, 촬영을 하기 위한 버튼 button_capture 에 리스너를 달아줍니다.

 

다음은 메인 액티비티 소스코드입니다.

 

 


public class MainActivity extends Activity{
    String TAG = "CAMERA";
    private Context mContext = this;
    private Camera mCamera;
    private CameraPreview mPreview;
    
    private PictureCallback mPicture = new PictureCallback() {
  @Override
  public void onPictureTaken(byte[] data, Camera camera) {
   // JPEG 이미지가 byte[] 형태로 들어옵니다
   
   File pictureFile = getOutputMediaFile();
   if (pictureFile == null) {
    Toast.makeText(mContext, "Error saving!!", Toast.LENGTH_SHORT).show();
    return;
   }

   try {
    FileOutputStream fos = new FileOutputStream(pictureFile);
    fos.write(data);
    fos.close();
    //Thread.sleep(500);
    mCamera.startPreview();
    
   } catch (FileNotFoundException e) {
    Log.d(TAG, "File not found: " + e.getMessage());
   } catch (IOException e) {
    Log.d(TAG, "Error accessing file: " + e.getMessage());
   } /*catch (InterruptedException e) {
    e.printStackTrace();
   }*/
  }
 }; 

        
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                                         WindowManager.LayoutParams.FLAG_FULLSCREEN);
                
        setContentView(R.layout.activity_main);
        
  mContext = this;
  // 카메라 인스턴스 생성
  if (checkCameraHardware(mContext)) {
   mCamera = getCameraInstance();

   // 프리뷰창을 생성하고 액티비티의 레이아웃으로 지정합니다
   mPreview = new CameraPreview(this, mCamera);
   FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
   preview.addView(mPreview);

   Button captureButton = (Button) findViewById(R.id.button_capture);
   captureButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
     // JPEG 콜백 메소드로 이미지를 가져옵니다
     mCamera.takePicture(null, null, mPicture);
    }
   });
  }
  else{
   Toast.makeText(mContext, "no camera on this device!", Toast.LENGTH_SHORT).show();
  }
        
    }

 /** 카메라 하드웨어 지원 여부 확인 */
 private boolean checkCameraHardware(Context context) {
     if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
         // 카메라가 최소한 한개 있는 경우 처리
      Log.i(TAG, "Number of available camera : "+Camera.getNumberOfCameras());
         return true;
     } else {
         // 카메라가 전혀 없는 경우 
      Toast.makeText(mContext, "No camera found!", Toast.LENGTH_SHORT).show();
         return false;
     }
 }
 
 /** 카메라 인스턴스를 안전하게 획득합니다 */
 public static Camera getCameraInstance(){
     Camera c = null;
     try {
         c = Camera.open(); 
     }
     catch (Exception e){
         // 사용중이거나 사용 불가능 한 경우
     }
     return c;
 }
 
 /** 이미지를 저장할 파일 객체를 생성합니다 */
 private static File getOutputMediaFile(){
     // SD카드가 마운트 되어있는지 먼저 확인해야합니다
     // Environment.getExternalStorageState() 로 마운트 상태 확인 가능합니다 

     File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
               Environment.DIRECTORY_PICTURES), "MyCameraApp");
     // 굳이 이 경로로 하지 않아도 되지만 가장 안전한 경로이므로 추천함.

     // 없는 경로라면 따로 생성한다.
     if (! mediaStorageDir.exists()){
         if (! mediaStorageDir.mkdirs()){
             Log.d("MyCamera", "failed to create directory");
             return null;
         }
     }

     // 파일명을 적당히 생성. 여기선 시간으로 파일명 중복을 피한다.
     String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
     File mediaFile;
     
     mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_"+ timeStamp + ".jpg");
     Log.i("MyCamera", "Saved at"+Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES));

     return mediaFile;
 }
 @Override
 public void onPause(){
  super.onPause();
  // 보통 안쓰는 객체는 onDestroy에서 해제 되지만 카메라는 확실히 제거해주는게 안전하다.
  
 }
}

주석을 하도 깨알 같이 달았더니 딱히 더 추가로 이야기 할게 없어 보이는군요.
파일 저장을 하기 전에 콜백 메소드에서 추가적으로 이미지 처리를 할 수도 있는데요, 이럴때는 콜백 메소드에서 직접 처리 할게 아니라 AsyncTask (다음 포스트에서 설명) 등을 사용하여 별도의 스레드를 생성하여 처리를 해주는것이 좋습니다. 

처리 시간이 일정 시간 (제조사 마다 다를텐데 보통 5sec 라고 합니다)을 넘어서면 안드로이드 os 상에서 바로 ANR을 띄워 버리고 (어플리케이션 응답없음 팝업 ) 강제 종료 시켜버립니다. 사실 그런 이유가 아니더라도 사진 한장 찍고 다음 사진 찍을 준비를 하는데 시간이 오래 걸리는것도 좋은 설계는 아니니까 추가적인 처리를 넣게 된다면 스레드를 따로 생성 해야겠죠.

4. 매니페스트 정리
이번에는 딱히 매니페스트에 복잡한 내용은 없고 그냥 하드웨어 사용 권한, 기능 필터(마켓에서 카메라가 없는 경우 아얘 검색되지 않도록 하는 기능의 코드) 등의 추가가 있습니다.


    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
    <uses-feature android:name="android.hardware.camera" />
 
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity android:name="MainActivity"
                  android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

여기서 액티비티 태그에 저는 아얘 스크린 방향을 가로모드로 지정해 버렸지만, 화면 방향은 이미 자바 메인액티비티 소스에서도 지정하고 있기 때문에 매니페스트에 추가 할 필요는 없었습니다. 단지 이렇게도 설정 가능하다는것 정도로 생각하시면 되겠네요.
이제 컴파일해서 기기에서 직접 실행해 보시고, 로그도 곳곳에 심어 두었으니 이클립스에서 LogCat을 통해 값의 변화나 내용을 확인 해보시는것도 도움이 될것 같습니다.

반응형

'안드로이드 개발' 카테고리의 다른 글

안드로이드 오픈소스 모음  (2) 2015.05.28
안드로이드 intent 클래스명으로 호출하기  (0) 2015.05.28
정적 변수와 메소드 (static)  (0) 2015.05.28
안드로이드 폰 정보  (0) 2015.05.27
Fake dragging  (0) 2015.05.22

WRITTEN BY
아카이시

,