티스토리 뷰

7장에서는 안드로이드에서 카메라로 사진을 찍고, 음악과 동영상을 재생하고, 사진을 보고 음성 녹음을 하는 등의 멀티미디어 기능에 대해 설명한다. 이번 포스팅에서는 이런 멀티미디어 기능 중에 가장 중요한 사진을 찍는 기능에 대해서 알아보려고 한다.

 

[카메라로 사진찍기]

카메라로 사진찍는 방법은 대표적으로 두 가지가 있다. 첫 번째로는 Intent를 이용해 내장되어 있는 카메라 앱을 띄우는 방법으로 이 방법을 사용했을 떄는 별다른 카메라 설정을 해 줄 필요가 없다는 장점이 있다. 두 번째로는 Surface view 라이브러리를 이용해 카메라 미리보기 화면을 자신의 앱 안에 넣는 것이다. 이 방법은 카메라 위에 증강현실을 표현할 아이콘이나 그래픽 등을 보여줄 수 있는 등 마음대로 변형을 할 수 있다는 장점이 있다.

 

Intent를 이용해 카메라앱 띄우는 방법

먼저, Intent를 이용해 간단히 카메라앱에서 사진을 찍은 후에 그 결과물을 저장하는 방법을 알아보자.

카메라앱에서 사진을 찍은 후에 그 결과물을 저장할 파일 만들기 위해서는 아래의 코드 두 줄만 추가하면 된다.

File sdcard = Environment.getExternalStorageDirectory();
file = new File(sdcard, "capture.jpg");

이렇게 사진을 찍어서 저장하고, 해당 사진을 다른 앱에서 파일을 공유하도록 하려면 내용 제공자를 사용해야 한다. 안드로이드 버전 7.0 이후부터는 file://로 시작하는 Uri 정보를 다른 앱에서 접근할수 없고, 반드시 content://로 시작하는 내용 제공자를 사용하도록 바뀌었다.

 

내용 제공자를 사용하기 위해 AndroidManifest.xml에 아래의 코드를 추가한다. 아래 코드는 FileProvider로 특정 폴더를 공유하는 데 사용하는 내용 제공자를 설정하는 코드이다.

<provider
    android:authorities="패키지명.fileprovider"
    android:name="androidx.core.content.FileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/external" />
</provider>

 

@xml/external의 파일을 만들기 위해 res 폴더 아래에 xml 폴더를 만든 뒤 external.xml 파일을 추가한다. external.xml 파일의 코드는 아래와 같이 설정한다.

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="sdcard" path="." />
</paths>

 

이렇게 하면 내용 제공자를 사용해서 찍은 사진을 저장할 파일 위치를 지정하는 코드를 아래와 같이 쓸 수 있다.

Uri fileUri = FileProvider.getUriForFile(this, "패키지명.fileprovider", file);

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
startActivityForResult(intent, 101);

 

FileProvider.getUriForFile() 메서드를 사용해 카메라 앱에서 공유하여 사용할 수 있는 파일 정보를 Uri 객체로 만들 수 있고, 이를 intent의 부가데이터로 넣어서 카메라 액티비티로 전달한다.

 

아래의 코드는 실행한 카메라 액티비티의 응답을 받는 부분으로, 옵션 중 inSampleSize = 8로 지정한 것은 비트맵 객체의 크기를 1/8로 축소한다는 옵션이다. 이렇게 비트맵 객체의 크기를 줄이지 않으면 카메라 해상도가 높은 요즘 핸드폰의 경우, 넘어온 비트맵 객체의 크기가 너무 커서 메모리가 부족해지면서 앱이 비정상 종료하는 일이 발생할 수도 있다.

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == 101 && resultCode == Activity.RESULT_OK) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inSampleSize = 8;
        Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
        imageView.setImageBitmap(bitmap);
    }
}

 

이제 이를 실행하기 위해서는 두 가지 작업이 더 남아 있다. 

하나는 카메라 앱을 띄우기 위한 권한을 설정하는 것이고, 다른 하나는 이 권한들이 위험 권한이기 때문에 위험 권한을 위한 설정과 코드를 추가해야 하는 것이다.

 

아래는 AndroidManifest.xml에 추가해야 할 권한들이다.

<uses-feature> 태그는 단말에 하드웨어 카메라가 반드시 있어야 앱이 실행될 수 있다는 것을 지정하는 것이다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-feature android:name="android.hardware.camera"
    android:required="true" />

<uses-feature>이 카메라 앱을 띄우기 위한 권한이 되고, <uses-permission>에 들어있는 READ_EXTERNAL_STORAGE와 WRITE_EXTERNAL_STORAGE는 앱에서 sd카드에 내용을 읽어오고, 쓰는 것을 위한 권한이기 때문에 넣어주어야 한다.

 

이제 자동으로 위험 권한을 부여하기 위한 외부 라이브러리를 추가해야 한다. 위험 권한에 대한 내용은 '[부스트코스 PJ3 정리노트] 화면 여러 개 만들기'의 마지막 부분에서 다룬 적 있다.

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    ...

    implementation 'com.github.pedroSG94:AutoPermissions:1.0.3'
}

 

마지막으로 MainActivity.java에 가서 다음 코드를 추가해준다.

public class MainActivity extends AppCompatActivity implements AutoPermissionsListener {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        
        AutoPermissions.Companion.loadAllPermissions(this, 101);
    }

    ...

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        AutoPermissions.Companion.parsePermissions(this, requestCode, permissions, this);
    }

    @Override
    public void onDenied(int i, String[] strings) {
        Toast.makeText(this, "permissions denied : " + strings.length, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onGranted(int i, String[] strings) {
        Toast.makeText(this, "permissions granted : " + strings.length, Toast.LENGTH_SHORT).show();
    }
}

 

 

Surface view를 이용해 카메라 미리보기 넣기

이제 두 번째 방법인 Surface view를 이용한 카메라 미리보기 기능을 알아보자. 이 방법은 원하는 대로 카메라 어플을 재구성할 수 있다는 장점이 있지만 그렇기 때문에 조금 복잡한 과정이 필요하다.

 

카메라 미리보기 기능을 구현하려면 일반 View가 아니라 Surface View를 사용해야 한다. 그런데 Surface View는 Surface Holder 객체에 의해 생성되고 제어된다. 아래 그림을 참고하자.

 

먼저, 카메라 미리보기를 위한 기능을 담당하는 서피스 뷰를 상속한 CameraSurfaceView 클래스의 코드를 보자.

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    SurfaceHolder holder;
    Context context;

    public CameraSurfaceView(Context context) {
        super(context);
        init(context);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context) {
        this.context = context;

        holder = getHolder();   	// 서피스 뷰 내에 있는 서피스 홀더 객체 참조
        holder.addCallback(this);	// SurfaceHolder.Callback을 implement했기 때문에 this로 참조 가능
    }
}

xml과 java를 위한 생성자 2개를 만들어 준다. 또한 서피스 홀더 객체를 맴버 변수로 선언해야 한다.

 

생성자에서 하는 일은 다음과 같다. getHolder() 메서드로 서피스 뷰에 있는 서피스 홀더 객체를 참조한다. addCallBack() 메서드를 호출해서 콜백 메서드를 구현한 클래스의 객체를 인자로 넣어주어야 한다. 그 이유는 서피스 뷰는 일반 뷰와 다르기 때문에 메모리에 생기거나 없어지거나 크기가 바뀔 때 일반 뷰처럼 onCreate()나 onSizeChanged() 이런 콜백 메서드가 호출되지 않고 별도의 콜백 메서드를 등록해 주어야 한다. 그 콜백 메서드를 this로 해 두면 해당 클래스가 SurfaceHolder.Callback을 구현하게 해야 한다. 그리고 나서 implement method로 나오는 콜백 메서드 3개(surfaceCreated(), surfaceChanged(), surfaceDestroyed())를 구현하면 된다.

 

 

서피스 홀더의 콜백 메서드를 구현한 코드는 다음과 같다.

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    ...
    Camera camera;

    ...

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        camera = Camera.open();

        try {
            camera.setPreviewDisplay(holder);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
        camera.startPreview();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
}

surfaceCreated()에서는 Camera를 open()하고, setPreviewDisplay()를 이용해 미리보기 화면을 설정한다. surfaceChanged()는 화면에 보여지기 전에 미리보기의 크기가 결정되는 시점에 콜백되는 함수로, startPreview()를 호출하면 미리보기에 픽셀을 뿌려준다. surfaceDestroyed()에서는 stopPreview()로 미리보기를 중지하고, release()로 리소스를 해제 해야한다. 카메라도 여러 프로그램에서 동시에 쓰게 되면 한쪽에서 락이 걸릴 경우 카메라를 사용할 수 없게 되기 때문에 release()를 해줘서 리소스를 해제하는 것이 중요하다.

 

※ 참고

  1. 서피스 뷰는 안드로이드 초기에 만든 것이라서 콜백 메서드지만 함수 명명규칙을 따르지 않아 on으로 시작하지 않는다. (헷갈리지 않게 주의)

  2. 현재 Camera2라는 새로운 카메라 객체가 만들어졌는데 이 새로운 객체는 예전 단말에는 지원이 안되는 부분이 있다. 

  3. 카메라 미리보기를 사용할 때는 일반적으로 뷰를 중첩시켜 사용한다.

    - 서피스 홀더의 유형이 SURFACE_TYPE_PUSH_BUFFERS일 때 그 위에 별도의 그래픽 그리기가 제한된다. 그렇기 때문에 다른 위젯이나 그래픽을 그 위에 올리거나 그리고 싶다면 또 다른 레이아웃을 서피스 뷰 위에 겹쳐 두고 배경을 투명하게 만드는 방법을 사용할 수 있다.

 

이제, 미리보기 화면을 설정했기 때문에 Camera 객체를 이용해 사진을 찍는 메서드를 서피스뷰에 다음과 같이 추가한다.

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    ...
    
    public boolean capture(Camera.PictureCallback callback) {
        if (camera != null) {
            camera.takePicture(null, null, callback);
            return true;
        }
        return false;
    }
}

 

 

이렇게 서피스 뷰를 상속한 CameraSurfaceView를 만들었으면, MainActivity.java에 이를 이용해 사진을 찍는 코드를 추가한다.

private void capture() {
    surfaceView.capture(new Camera.PictureCallback() {
        @Override
        public void onPictureTaken(byte[] bytes, Camera camera) {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inSampleSize = 8;
            Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);

            imageView.setImageBitmap(bitmap);

            camera.startPreview();
        }
    });
}

위의 코드는 사진을 찍은 결과를 처리하는 코드가 PictureCallBack 인터페이스를 구현하는 부분에 들어있다. 이 인터페이스는 CameraSurfaceView에 정의된 capture() 메서드를 호출할 때 전달된다. 즉, 사진을 찍을 때 자동으로 호출되는 onPictureTaken() 메서드로 캡처한 이미지 데이터가 전달되는 것이다. 여기서 inSampleSize = 8로 설정해서 비트맵 크기를 1/8로 줄여주었다. 또한 사진을 찍으면 미리보기가 중지되기 때문에 마지막에 startPreview() 메서드를 호출해서 미리보기를 다시 시작해 주어야 한다.

 

 

마지막으로, AndroidManifest.xml에 필요한 권한들을 추가하고 앞에서 했던 위험권한을 설정해 준다. 

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<uses-feature android:name="android.hardware.camera"
    android:required="true" />

 

이렇게 하면 이제 서피스 뷰를 이용한 앱 내에 카메라 미리보기 화면을 모두 구현한 것이다.

 

하지만 직접 실행을 해 보면 미리보기 화면이 90도 돌아가 있는 것을 볼 수 있다. 이는 서피스 뷰의 카메라 미리보기의 기본 모드는 가로 모드(Landscape mode)이기 때문에 미리보기 화면이 돌아가 있는 것이다. 이를 세로 모드로 보이게 하고 싶다면 CameraSurfaceView 안의 surfaceCreated() 메서드를 수정하고 setCameraOrientation() 메서드를 아래와 같이 추가하면 된다.

@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
    camera = Camera.open();
    setCameraOrientation();
    
    try {
        camera.setPreviewDisplay(holder);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private void setCameraOrientation() {
    if (camera == null) return;

    Camera.CameraInfo info = new Camera.CameraInfo();
    Camera.getCameraInfo(0, info);

    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    int rotation = manager.getDefaultDisplay().getRotation();

    int degrees = 0;
    switch (rotation) {
        case Surface.ROTATION_0: degrees = 0; break;
        case Surface.ROTATION_90: degrees = 90; break;
        case Surface.ROTATION_180: degrees = 180; break;
        case Surface.ROTATION_270: degrees = 270; break;
    }

    int result;
    if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
        result = (info.orientation + degrees) % 360;
        result = (360 - result) % 360;
    } else {
        result = (info.orientation - degrees + 360) % 360;
    }

    camera.setDisplayOrientation(result);
}

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함