일모도원(日暮途遠) 개발자
[Android 개발] 카메라 앱 기본 기능 본문
카메라 기능을 구현하기 위하여 공부하던중, 아래에 있는 코드는 Kotlin으로 되어 있어, 사진 찍고 이미지 파일을 저장하는 부분까지만 자바로 구현해보았다. (자바 11 사용중)
https://developer.android.com/codelabs/camerax-getting-started?hl=ko#0
먼저 Empty Views Activity를 선택하자.
이름은 적당히주자. 난 package이름을 com.android.example.camerajavaapp로 하였다.
만약 빈 프로젝트를 만들었는데, 에러가 발생하면 여기를 참고하자.
앱 모듈의 build.gradle의 dependencies에 아래를 추가하자. 현재 최신 버전은 1.2.1이다.
def camerax_version = "1.2.1"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
앱 모듈의 build.gradle의 android {에는 viewBinding을 추가하고 Sync Now를 클릭하자.
buildFeatures {
viewBinding true
}
activity_main.xml 파일은 아래내용으로 교체하자. 나는 image_capture_button 버튼만 사용한다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/image_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:elevation="2dp"
android:text="@string/take_photo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
strings.xml도 아래처럼 업데이트 하자. 앱이름은 본인껄로 수정해도 된다
<resources>
<string name="app_name">Dalnim CameraXApp</string>
<string name="take_photo">Take Photo</string>
</resources>
AndroidManifest.xml에 필요한 권한을 요청하자. 안드로이드 SDK 29이상이면 WRITE_EXTERNAL_STORAGE는 요청안해도 된다.
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
카메라 권한 요청 코드. 카메라 권한을 얻으면, startCamera() 메소드를 실행한다.
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE_PERMISSIONS = 10;
private String[] REQUIRED_PERMISSIONS = {
android.Manifest.permission.CAMERA
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 카메라 권한이 있으면 카메라 실행. 없으면 카메라 권한 요청
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
}
}
//카메라 권한이 있는지 검사
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
//카메라 권한 요청 결과 처리
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera();
} else {
Toast.makeText(this, "유저가 권한을 주어야 사용가능합니다.", Toast.LENGTH_SHORT).show();
finish();
}
}
}
}
카메라를 실행하고 Preview에 카메라에 찍히는 영상을 보여주는 코드.
만약 사진 저장이 필요없으면, 다음처럼 imageCapture 인자를 생략하면 된다. (imageCapture관련 코드도 필요없어짐)
cameraProvider.bindToLifecycle(MainActivity.this, cameraSelector, preview);
public class MainActivity extends AppCompatActivity {
private ImageCapture imageCapture;
private ExecutorService cameraExecutor;
//카메라를 실행하고 preview에 촬영되고 있는 영상을 보여준다.
private void startCamera() {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(new Runnable() {
@Override
public void run() {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
// Preview
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(viewBinding.viewFinder.getSurfaceProvider());
imageCapture = new ImageCapture.Builder().build();
// 후면 카메라 선택
CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
// 기존에 바인드 된게 있으면 해제부터 한다.
cameraProvider.unbindAll();
// 카메라의 생명주기를 이 액티비티와 동일하게 한다. (사진 저장이 필요없으면, imageCapture 인자를 생략하면 된다.)
cameraProvider.bindToLifecycle(
MainActivity.this, cameraSelector, preview, imageCapture);
} catch (ExecutionException | InterruptedException exc) {
Log.e(TAG, "카메라 뷰어 바인딩 실패", exc);
}
}
}, ContextCompat.getMainExecutor(this));
}
}
버튼을 눌러서 사진을 찍고, 파일로 저장하는 코드. contentValues와 outputOptions은 파일을 저장하기 위한 정보를 담는다.
//사진을 찍어서 파일로 저장을 한다.
private void takePhoto() {
if (imageCapture == null) {
return;
}
ContentValues contentValues = getContentValues();
ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build();
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageSavedCallback() {
@Override
public void onError(@NonNull ImageCaptureException exc) {
Log.e(TAG, "사진 저장 실패 : " + exc.getMessage(), exc);
}
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
String msg = "사진 저장 성공 : " + output.getSavedUri();
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
Log.d(TAG, msg);
}
}
);
}
//찍은 사진이 저장될 경로
@NonNull
private static ContentValues getContentValues() {
String name = new SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis());
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Dalnim-CameraX-Image");
}
return contentValues;
}
만약 캡쳐된 이미지를 저장하지 않고, 바로 사용할경우는 outputOptions을 넘기지 않고, OnImageSavedCallback대신 OnImageCapturedCallback을 호출하면 된다.
//캡쳐된 이미지를 저장하지 않고, 바로 사용할경우는 outputOptions을 넘기지 않고, OnImageCapturedCallback을 호출하면 된다.
imageCapture.takePicture(
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageCapturedCallback() {
@Override
public void onCaptureSuccess(ImageProxy image) {
super.onCaptureSuccess(image);
//캡쳐된 image파일을 사용하면 된다.
Log.e(TAG, "사진 캡처 성공");
}
@Override
public void onError(ImageCaptureException exception) {
Log.e(TAG, "사진 캡처 실패 : " + exception.getMessage(), exception);
}
});
전체 소스는 아래를 참고하자.
https://github.com/dalnim/CameraJavaApp
package com.android.example.camerajavaapp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
import com.android.example.camerajavaapp.databinding.ActivityMainBinding;
import com.google.common.util.concurrent.ListenableFuture;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding viewBinding;
private ImageCapture imageCapture;
private ExecutorService cameraExecutor;
private static final String TAG = "CameraXApp";
private static final String FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
private static final int REQUEST_CODE_PERMISSIONS = 10;
private String[] REQUIRED_PERMISSIONS = {
android.Manifest.permission.CAMERA
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
// 카메라 권한이 있으면 카메라 실행. 없으면 카메라 권한 요청
if (allPermissionsGranted()) {
startCamera();
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
}
viewBinding.imageCaptureButton.setOnClickListener( v -> takePhoto());
cameraExecutor = Executors.newSingleThreadExecutor();
}
//사진을 찍어서 파일로 저장을 한다.
private void takePhoto() {
if (imageCapture == null) {
return;
}
ContentValues contentValues = getContentValues();
ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build();
//캡쳐된 이미지를 저장하지 않고, 바로 사용할경우는 outputOptions을 넘기지 않고, OnImageCapturedCallback을 호출하면 된다.
imageCapture.takePicture(
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageCapturedCallback() {
@Override
public void onCaptureSuccess(ImageProxy image) {
super.onCaptureSuccess(image);
//캡쳐된 image파일을 사용하면 된다.
Log.e(TAG, "사진 캡처 성공");
}
@Override
public void onError(ImageCaptureException exception) {
Log.e(TAG, "사진 캡처 실패 : " + exception.getMessage(), exception);
}
});
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
new ImageCapture.OnImageSavedCallback() {
@Override
public void onError(@NonNull ImageCaptureException exc) {
Log.e(TAG, "사진 저장 실패 : " + exc.getMessage(), exc);
}
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
String msg = "사진 저장 성공 : " + output.getSavedUri();
Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
Log.d(TAG, msg);
}
}
);
}
//찍은 사진이 저장될 경로
@NonNull
private static ContentValues getContentValues() {
String name = new SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis());
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Dalnim-CameraX-Image");
}
return contentValues;
}
//카메라를 실행하고 preview에 촬영되고 있는 영상을 보여준다.
private void startCamera() {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(new Runnable() {
@Override
public void run() {
try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
// Preview
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(viewBinding.viewFinder.getSurfaceProvider());
imageCapture = new ImageCapture.Builder().build();
// 후면 카메라 선택
CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
// 기존에 바인드 된게 있으면 해제부터 한다.
cameraProvider.unbindAll();
// 카메라의 생명주기를 이 액티비티와 동일하게 한다. (사진 저장이 필요없으면, imageCapture 인자를 생략하면 된다.)
cameraProvider.bindToLifecycle(
MainActivity.this, cameraSelector, preview, imageCapture);
} catch (ExecutionException | InterruptedException exc) {
Log.e(TAG, "카메라 뷰어 바인딩 실패", exc);
}
}
}, ContextCompat.getMainExecutor(this));
}
//카메라 권한이 있는지 검사
private boolean allPermissionsGranted() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
//카메라 권한 요청 결과 처리
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera();
} else {
Toast.makeText(this, "유저가 권한을 주어야 사용가능합니다.", Toast.LENGTH_SHORT).show();
finish();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
imageCapture = null;
cameraExecutor.shutdown();
}
}
'안드로이드 개발 > 안드로이드' 카테고리의 다른 글
판매자 ID 찾기 (0) | 2024.10.15 |
---|---|
[안드로이드개발] ExoPlayer 백그라운드에 오래있으면 화면 먹통되는 현상 (0) | 2023.05.04 |
[안드로이드 개발] 클립보드에 텍스트가 있는데 hasPrimaryClip이 false가 날때 (0) | 2023.04.19 |
[안드로이드 개발] 아직도 헷갈리는 안드로이드 폴더 구조. (0) | 2023.04.07 |
[안드로이드UI] 채팅 대화내역 키보드에 안가리게 하기. (0) | 2023.04.01 |