카메라 앱 화면 보여주기
사진을 찍는 가장 간단한 방법은 단말에 이미 설치된 카메라 앱을 사용하는 것입니다.
이미 만들어져 있는 화면이니 여러분이 직접 화면을 만들 필요가 없죠.
다른 앱의 화면을 보여주어야 하므로 인텐트를 이용해 시스템으로 요청하는 과정이 필요합니다.
사용자가 사진찍기 버튼을 누르면 인텐트 객체를 만들고 startActivityForResult 메소드를 이용해 시스템으로 인텐트 객체를 전달합니다.
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
인텐트 객체를 만들 때는 MediaStore.ACTION_IMAGE_CAPTURE라는 액션 정보를 전달합니다.
그리고 부가데이터로 MediaStore.EXTRA_OUTPUT을 추가합니다.
이 부가데이터의 값으로는 카메라 앱에서 찍은 사진을 저장할 위치를 넣어줍니다.
Uri라는 자료형으로 되어 있어야 하므로 Uri.fromFile 메소드를 이용해 파일의 Uri를 지정합니다.
이렇게 요청하면 카메라 앱의 화면이 보이고 사진을 찍을 수 있습니다.
사진 찍은 결과 보여주기
사진을 찍고 나면 다시 여러분이 만든 앱의 화면으로 돌아오며 이때 찍은 사진을 확인할 수 있습니다.
새로 띄운 화면에서 돌아오는 과정이므로 onActivityResult 메소드를 재정의하면 이 메소드가 자동으로 호출됩니다.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
imageView.setImageBitmap(bitmap);
}
}
onActivityResult 메소드 안에서 파일을 확인하면 찍은 사진이 저장되어 있다는 것을 알 수 있습니다.
이것을 이미지뷰에 설정하면 사진이 화면에 보이게 됩니다.
카메라 미리보기 추가하기
여러분이 만든 화면에 카메라 미리 보기를 추가하고 싶다면 SurfaceView를 사용하면 됩니다.
이 뷰는 일반적인 뷰와 달라서 만들어야 하는 코드가 조금 더 많습니다.
SurfaceView를 상속하는 새로운 클래스를 만들고 이 클래스가 SurfaceHolder의 Callback 인터페이스를 구현하도록 합니다.
SurfaceView는 껍데기 역할만 하고 실제 컨트롤은 SurfaceHolder가 담당합니다.
그리고 뷰의 상태 변화를 알 수 있도록 Callback 인터페이스를 제공합니다.
이 인터페이스를 구현하면 뷰가 만들어졌을 때, 변경될 때, 없어질 때 자동으로 메소드가 호출됩니다.
private class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder mHolder;
public CameraSurfaceView(Context context) {
super(context);
mHolder = getHolder();
mHolder.addCallback(this);
}
public void surfaceCreated(SurfaceHolder holder) {
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
SurfaceView 객체가 메모리에 만들어질 때 자동으로 호출되는 surfaceCreated 메소드 안에서는 카메라 객체를 오픈하고(코드 라인:2) 카메라 객체에 서피스홀더 객체를 설정합니다(코드 라인:4).
public void surfaceCreated(SurfaceHolder holder) {
camera = Camera.open();
try {
camera.setPreviewDisplay(mHolder);
} catch (Exception e) {
Log.e("CameraSurfaceView", "Failed to set camera preview.", e);
}
}
Camera 객체는 단말의 하드웨어 카메라를 참조하며 이 클래스 안에 변수로 선언되어 있습니다.
뷰의 크기가 변경되는 시점에는 미리 보기 화면이 보이도록 만들어줍니다.
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
camera.startPreview();
}
뷰가 메모리에서 사라지는 시점에는 카메라 참조를 해제합니다.
capture 메소드는 이 객체를 통해 사진을 찍을 수 있도록 합니다.
public void surfaceDestroyed(SurfaceHolder holder) {
camera.stopPreview();
camera.release();
camera = null;
}
public boolean capture(Camera.PictureCallback handler) {
if (camera != null) {
camera.takePicture(null, null, handler);
return true;
} else {
return false;
}
}
카메라 미리 보기 등의 기능을 구현할 때는 권한이 필요하므로 AndroidManifest.xml 파일에 다음 권한을 추가합니다.
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
만들어진 객체 사용하기
버튼을 눌렀을 때 카메라 미리 보기를 위해 만들었던 객체의 capture 메소드를 호출하도록 합니다.
그러면 사진을 찍은 결과를 바이트 배열로 받을 수 있습니다.
public void onClick(View v) {
cameraView.capture(new Camera.PictureCallback() {
public void onPictureTaken(byte[] data, Camera camera) {
try {
Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);
사진을 찍은 결과를 비트맵 객체로 변환했으니 이미지뷰에 설정하면 화면에 보여줄 수 있습니다.
생각해보기
- 앨범에 저장된 사진을 가져올 수도 있을까요?
- 카메라 미리 보기 화면에 보이는 영상을 연속해서 몇 장 저장할 수도 있을까요?
참고 자료
comment
android.os.FileUriExposedException 개요
`file://` 형태의 URI가 다른 앱에 노출되었을 경우 발생합니다.
만일 상대방 앱에 Manifest.permission.READ_EXTERNAL_STORAGE 권한이 없는 경우에도
노출된 URI를 공유하게 만들기 때문입니다
그러므로 `content://` URI를 이용하는 게 좋습니다. 타겟 SDK 버전을 낮추어 `file://` URI를 공유할 순 있으나, 이 방법은 몹시 권장하지 않습니다.
--
FileProvider 개요
`content://` URI 생성을 통해서 파일을 안전하게 공유할 수 있게 합니다. Content URI를 함유하는 Intent를 만들어 상대 앱에 전달할 때 Intent의 setFlags()를 사용하여 권한을 추가해 줄 수 있습니다.
반면에, `file://` URI로 파일을 접근하게 하면, 파일 자체의 접근권한을 수정하게 되고, 제 3의 앱이 그 권한을 이용할 수 있는 부수효과가 발생합니다. 그러므로 이 방식은 안전하지 않습니다.
--
1. manifest의 <appication> 내부에 <provider>를 추가한다.
2. res>xml>file_paths.xml 을 만들고 <paths> 태그를 작성한다. xml 파일명이나 위치는 자유롭게 할 수 있다. manifiest의 메타 데이터만 알맞게 변경해주면 된다.
3. content URI로 상대 앱에 넘겨주기
4. 이미지 뷰에 그리기
2018년 하반기부터 targesdkversion을 26이하로 설정을 못하게 해놔서 강의를 따라하기가 힘든데요, 두 번쨰 영상은 3. 화면 여러 개 만들기 강의에서 위험권한 부여하기 강의를 그대로 따라하면서 CAMERA 에 대한 권한을 사용자로부터 얻게끔만 하면 잘 실행되는 것을 확인했습니다. 위에 두번째 방법 강의 코드 그대로 따라하고 앱이 실행되자마자 카메라 사용권한을 사용자에게 얻는 코드를 <위험 권한 부여하기> 강의를 참고해서 작성하면 잘 실행이 됩니다.
비슷한 오류가 발생하시는 분들이 있을거같아서 구글링해서 얻은 결과를 공유합니다.
API 24부터는 uri.fromfile 을 사용하면 FileUriExposedException이 발생한다고 합니다.
이경우 uri.fromfile 대신 fileprovider을 사용해야하는데
https://medium.com/@ali.muzaffar/what-is-android-os-fileuriexposedexception-and-what-you-can-do-about-it-70b9eb17c6d0
xml파일도 추가해야하고 방법도 복잡한데.. 따라해본결과 이유는 알수없지만 카메라를 띄운후 엑티비티로 부터 받는 RESULT CODE값이 RESULT_CANCELED로 나와서 이미지뷰에 이미지를 띄울수가 없었습니다.
제가 찾은 간단하게 하는 방법으론
코드 위에
위 코드를 추가하고 그대로 uri.fromfile(file)을 사용하니 카메라 엑티비티로 부터 받는값이 RESULT_OK가 되었습니다.
하지만 bitmapFactory에서 FileNotFoundException storage/emulated permission denied 에러가 나면서 이미지뷰에 이미지가 뜨지않았습니다. 해결방법으론 Runtime permission 을 implement 해야한다는데, 방법이 너무 복잡해서 직접 에뮬레이터 설정에 들어가서 해당어플 storage 접근권한을 켜줬더니 정상적으로 이미지가 뜨게 되었습니다.
저와 비슷한 오류를 경험하시는 분들이 있을거 같아서 글을남깁니다. 저도 이제 공부하고 있는 입장이라 혹시 FIleProvider 를 통해 Uri를 얻는 방법과, 코드로 직접 접근권한을 승인할 수 있는 방법을 아신다면 답글로 남겨주시면 정말 감사하겠습니다.
두 번째 방법으로 했는데 왜 어플이 바로 꺼져 버리는 건가요 ㅠㅠ 참고로 코드는 똑같이 썼습니다!!
android.os.FileUriExposedException: file:///storage/emulated/0/capture.jpg exposed beyond app through ClipData.Item.getUri() 첫번째 영상 따라하다가 오류가나네요. 저같은경우는 위험권한 체크하는 방식으로 하려했더니 (영상처럼 SDK 버전을 낮추지 않았습니다) 찾아보니 API 24 버전부터 정책이 바뀌어서 다른 방식으로 해야한다고하네요... 다른분들 참고하셨으면 해서 댓글 남깁니다!ㅎㅎㅎ