이전 Android 포스팅인 "[Android] 홍드로이드 기초 강의 - Camera 촬영 및 Image 가져오기_환경설정"에 이어서 MainActivity에 대해 작성하겠습니다.
1. 애플리케이션 동작
기본적인 동작은 간단합니다.
1) 사용자로부터 권한을 요청한다.
2) 촬영 버튼을 눌러 카메라를 실행한다.
3) 사진을 촬영하고, 앱 메인화면으로 가져온다.
1)
애플리케이션의 첫 화면인 MainActivity가 나타나면 권한이 허용되었는지 확인해야 합니다.
권한이 거부 상태이면 사용자에게 권한 요청 메시지가 보입니다.
2)
권한을 허용하면 앱 하단의 '촬영' 버튼을 이용해서 카메라를 사용할 수 있습니다.
3)
촬영한 이미지는 메인화면에 표시됩니다.
이때 어떻게 스마트폰을 들고 있느냐에 따라서 이미지가 90˚, 180˚ 등으로 돌아갈 것입니다.
회전된 이미지를 되돌리기 위해서 ExifInterface로 부터 회전각도를 가져옵니다. 1
2. 권한 체크
권한을 체크하는 방법은 다음의 코드와 같습니다.
// 권한 체크
TedPermission.with(getApplicationContext())
.setPermissionListener(permissionListener)
.setRationaleMessage("카메라 권한이 필요합니다.")
.setDeniedMessage("거부하셨습니다.")
.setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)
.check();
권한 체크를 수행하는 데에 필요한 매개변수가 많아서 '빌더 패턴(Builder pattern)'을 활용한 코드입니다. 2
상세 설명이 필요한 부분을 하나씩 뜯어보겠습니다.
TedPermission.with(getApplicationContext())
1) with()가 context를 매개변수로 받아서 빌더(Builder)를 반환해주는 TedPermission 라이브러리의 메소드입니다.
2) getApplicationContext()은 애플리케이션이 종료되기 전까지 사용될 수 있는 context를 반환해줍니다. 3
카메라 기능을 이용할 때마다 사용자에게 권한 요청 메시지를 보여줄 필요가 없습니다.
게다가 권한을 허용한 상태라면 불편만 늘어날 겁니다.
따라서 권한 체크는 애플리케이션을 처음 이용할 때 단 한 번만 요청하면 되므로 Application Context를 사용합니다.
※ Activity 생존 주기를 지니는 context가 필요하면 getContext()
.setPermissionListener(permissionListener) // permissionListener 호출
.setRationaleMessage("카메라 권한이 필요합니다.") // 권한 요청 시에 나타나는 메시지
.setDeniedMessage("거부하셨습니다.") // 거부하면 나타나는 메시지
.setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA) // 외부 저장소 쓰기 권한 및 카메라 권한
.check();
1) setPermissionListener(permissionListener)는 권한이 허가되거나 거부됐을 때 결과를 리턴해주는 리스너를 설정합니다.
2) setPermissions()는 필요한 권한들을 설정합니다. 현재 외부 저장소 쓰기 권한과 카메라 권한을 요청하고 있습니다.
※ 외부 저장소 쓰기가 암묵적으로 읽기 권한도 허용
끝으로 아래의 PermissionListener를 정의해주면 권한 체크는 마무리됩니다.
// 권한 승인/거절 여부에 따른 안내 메시지
PermissionListener permissionListener = new PermissionListener() {
@Override
public void onPermissionGranted() {
Toast.makeText(getApplicationContext(), "권한이 허용됨",Toast.LENGTH_SHORT).show();
}
@Override
public void onPermissionDenied(List<String> deniedPermissions) {
Toast.makeText(getApplicationContext(), "권한이 거부됨",Toast.LENGTH_SHORT).show();
}
};
3. 캐시 파일 생성 및 카메라 실행
카메라로 사진을 촬영하므로 공용 외부 저장소에 접근해야 하고 갤러리에 저장할 필요는 없으므로 임시 디렉터리를 이용합니다.
그런 다음에는 중복되지 않는 파일 이름을 만들어야 합니다.
또한 나중에 사용할 수 있도록 멤버 변수에 경로를 저장할 수도 있습니다.
아래의 코드는 날짜-시간 스탬프를 활용해서 사진의 고유한 파일 이름을 반환하는 메소드입니다.
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "TEST_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName,
".jpg",
storageDir
);
imageFilePath = image.getAbsolutePath();
return image;
}
1) getExternalFilesDir (Environment.DIRECTORY_PICTURES 4)는 사진을 앱 이외에는 비공개로 설정합니다.
2) File.createTempFile()는 임시파일을 생성할 대 사용합니다. 매개변수로 파일명, 확장자, 경로를 가집니다.
3) imageFilePath는 Bitmap 이미지를 만들 때 사용될 절대 경로입니다.
위의 코드를 통해 캐시 파일을 만들고 나면 카메라 앱을 실행할 준비를 해야 합니다.
아래의 코드가 바로 카메라를 실행하는 코드입니다.
// 촬영 버튼 눌렀을 때
findViewById(R.id.btn_capture).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 기본 카메라 생성
if(intent.resolveActivity(getPackageManager()) != null) { // 기본 카메라를 이용할 수 있다면
File photoFile = null;
try {
photoFile = createImageFile(); // 캐시 파일을 받아옴
} catch(IOException e) {
}
if(photoFile != null) {
photoUri = FileProvider.getUriForFile(getApplicationContext(), getPackageName(), photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
}
}
});
1) resolveActivity()는 매개변수로 주어진 Activity가 애플리케이션 목록에서 실행 가능한지 확인하는 메소드입니다.
2) getPackageManager()는 스마트폰에서 사용 가능한 앱들의 목록을 가져옵니다.
첫 조건문은 위의 설명에 의해 기본 카메라를 이용할 수 있는지 판단하는 조건문입니다.
카메라를 사용할 수 있다면 캐시 파일을 생성해서 저장합니다.
이후에 애플리케이션이 캐시 파일에 대한 시스템 사용 권한을 uri로 생성하고 카메라 intent에 저장합니다.
4. 이미지 조정 및 앱으로 가져오기
카메라로 촬영한 이미지를 앱으로 가져오는 마지막 단계입니다.
촬영하는 방법에 따라 사진이 돌아가는 경우가 있습니다.
회전된 이미지를 원래대로 돌리기 위해선 얼마큼 회전됐는지 알아야 합니다.
private int exifOrientationToDegress(int exifOrientation) {
if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
return 90;
} else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
return 180;
} else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
return 270;
}
return 0;
}
Exifinterface가 제공하는 회전 각도를 매개변수로 가져와서 회전된 정도를 반환해줍니다. 5
알아낸 회전각도로 이미지를 조정하고 반환해줍니다.
private Bitmap rotate(Bitmap bitmap, float degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
마무리로 촬영이 끝났을 때 이미지를 앱으로 가져오도록 onActivityResult()를 정의해줍니다.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath); // 캐시 파일에 대한 bitmap 생성
ExifInterface exif = null;
try{
exif = new ExifInterface(imageFilePath);
}catch(IOException e) {
e.printStackTrace();
}
int exifOrientation;
int exifDegree;
if(exif != null) {
exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
exifDegree = exifOrientationToDegress(exifOrientation); // 회전 각도
} else {
exifDegree = 0;
}
// 이미지를 조정하고 화면에 띄우기
((ImageView) findViewById(R.id.iv_result)).setImageBitmap(rotate(bitmap, exifDegree));
}
}
5. 전체코드 및 실행
package com.example.cameraexample_fixed;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.security.Permission;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.gun0912.tedpermission.PermissionListener;
import com.gun0912.tedpermission.TedPermission;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_IMAGE_CAPTURE = 672; // 다수의 activity가 호출되었을 때, 이를 구별하기 위한 상수값
private String imageFilePath;
private Uri photoUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 권한 체크
TedPermission.with(getApplicationContext())
.setPermissionListener(permissionListener)
.setRationaleMessage("카메라 권한이 필요합니다.")
.setDeniedMessage("거부하셨습니다.")
.setPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA)
.check(); // 설정한 권한 체크
// 촬영 버튼 눌렀을 때
findViewById(R.id.btn_capture).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 기본 카메라 생성
if(intent.resolveActivity(getPackageManager()) != null) {
File photoFile = null;
try {
photoFile = createImageFile(); // 캐시 파일을 받아옴
} catch(IOException e) {
}
if(photoFile != null) {
photoUri = FileProvider.getUriForFile(getApplicationContext(), getPackageName(), photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
}
}
});
}
private File createImageFile() throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "TEST_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile( // 캐시된 파일 생성
imageFileName,
".jpg",
storageDir
);
imageFilePath = image.getAbsolutePath(); // 생성된 캐시 파일의 절대 경로
return image; // 캐시 파일 반환
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
//super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);
ExifInterface exif = null;
try{
exif = new ExifInterface(imageFilePath);
}catch(IOException e) {
e.printStackTrace();
}
int exifOrientation;
int exifDegree;
if(exif != null) {
exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
exifDegree = exifOrientationToDegress(exifOrientation);
} else {
exifDegree = 0;
}
((ImageView) findViewById(R.id.iv_result)).setImageBitmap(rotate(bitmap, exifDegree));
}
}
// Rotate된 이미지를 촬영 당시 시점으로 되돌릴 각도 반환
private int exifOrientationToDegress(int exifOrientation) {
if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
return 90;
} else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_180) {
return 180;
} else if(exifOrientation == ExifInterface.ORIENTATION_ROTATE_270) {
return 270;
}
return 0;
}
// 화면 각도 변환
private Bitmap rotate(Bitmap bitmap, float degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
// 권한 승인/거절 여부에 따른 안내 메시지
PermissionListener permissionListener = new PermissionListener() {
@Override
public void onPermissionGranted() {
Toast.makeText(getApplicationContext(), "권한이 허용됨",Toast.LENGTH_SHORT).show();
}
@Override
public void onPermissionDenied(List<String> deniedPermissions) {
Toast.makeText(getApplicationContext(), "권한이 거부됨",Toast.LENGTH_SHORT).show();
}
};
}
정상적으로 동작함을 알 수 있습니다.
기초 강의라고 하기엔 많은 부분들이 설명이 생략된 채로 강의가 진행되어 정리하기 바빴던 예제였습니다.
프로젝트는 GitHub에도 업로드했습니다.
※참고
https://bictoselfdev.blogspot.com/2021/02/builderPattern.html
https://4z7l.github.io/2020/09/17/android-context.html
https://mainia.tistory.com/1179
https://developer.android.com/reference/android/media/ExifInterface#constants
https://justbobby.tistory.com/12
https://codechacha.com/ko/check-if-startable-activity/
https://billcorea.tistory.com/12
'Android' 카테고리의 다른 글
[Android] Android studio Arctic Fox 버전 이후 build.gradle 수정하기 (0) | 2022.05.13 |
---|---|
[Android] 홍드로이드 기초 강의 - 리사이클러뷰(RecyclerView) (2) | 2022.03.13 |
[Android] 홍드로이드 기초 강의 - Camera 촬영 및 Image 가져오기_환경설정 (0) | 2022.02.26 |
[Android] startActivity()와 startActivityForResult()의 차이점 (0) | 2022.02.24 |
[Android] 홍드로이드 기초 강의 - SharedPreferences로 데이터 세이브/로드 (0) | 2022.02.21 |