반응형

안녕하세요. Simple& Happy Dev입니다.

이번에는 Camera2 API 이용해서 손전등 앱을 만들어 보도록 하겠습니다.
앞선 글에서 Camera2 API 이용하는 방법은 다른 방법들 대비해서 장점이 없기 때문에 일반적으로 사용하지 않는다고 말씀드렸습니다.

그래도, 궁금해하실 분이나 필요하실 분도 계실 것 같아서 글을 작성해봅니다.

[Camera2 API 이용한 손전등 앱]

안드로이드 초기 버전부터 사용되던 Camera API는 Android 5.0(API Level: 21)이 되어서야 Camera2 API로 Camera Framework이 변경되면서 구조적으로 큰 변화가 생기게 됩니다. 그 이전까지는 Camera API에서 기본 Feature(전면 카메라 지원, 얼굴인식 지원, 측광/초점 영역 지원, CAF 지원, AE/AWB Lock 지원 등) 추가하는 데 집중되었습니다.
(혹, 오해하실까 봐 말씀드리면, 위의 Feature들은 그 당시 단말기 업체의 카메라 앱에서는 추가되기 전부터 이미 있던 기능들입니다. 다만, 안드로이드 SDK에 API가 없어서 일반 앱 개발자들이 해당 기능들이 들어간 앱을 만들 수가 없었던 것이죠. 안드로이드 SDK에서 API로 지원하지 않는데, 단말기 업체에서는 어떻게 기능을 넣을 수 있게 된 건지에 대해서는 나중에 별도로 다룰 예정입니다)

Camera2 API로 넘어오면서 Camera Framework의 큰 구조적 변화는 성능 개선과 새로운 Feature 구현이 목적이었습니다.
플래시의 경우 이런 변화에 크게 영향은 없지만, 플래시를 제어하기 위한 접근이 조금 더 복잡해졌습니다.

아래는 여기에서 다루고 있는 예제로 만든 앱의 Sequence Diagram입니다. 이거 만드는 데 시간이 오래 걸렸습니다. :)

이후에 소스를 볼 때 아래 그림을 참조하면서 따라오시면 됩니다.

원본이미지 다운받기
FlashlightAppUsingCamera2API_sequence_diagram.zip

 

[소스 설명]

레이아웃 (activity_main.xml) 

화면 한가운데 별 모양 버튼을 하나 추가합니다. 별 모양 버튼은 SDK에 내장된 리소스를 사용하면 됩니다.
향후 이 버튼을 누르면 노란색 별모양으로 변하면서 플래시가 켜지게 할 예정입니다.
노란색 별모양은 자바 소스에서 버튼 클릭되면 변경되도록 처리할 것입니다. (이전 글의 레이아웃과 같습니다)

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">

<ImageButton
android:id="@+id/ibFlashOnOff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/btn_star_big_off"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>


자바 (MainActivity.java)

onCreate에서는 기기에 플래시가 지원하지 않으면 메시지 출력하고 3초 후 종료하도록 처리(delayedFinish())하도록 만들어 줍니다.
또한 별 모양 버튼에 대한 클릭 리스너를 만들어서 클릭 시 플래시를 켜주거나 꺼주고(flashlight()), 플래시 켜짐 상태에 따른 별 모양 이미지를 변경하도록 처리해줍니다. (여기까지는 이전 글의 소스와 같습니다)

카메라 특성 정보를 얻고, 카메라 연결을 위해서는 먼저 CameraManager 객체의 인스턴스(mCameraManager)를 얻어야 합니다.

그 아래에 이미지 버튼의 클릭 리스너와 클릭시의 플래시 동작 및 이미지 버튼의 모양 변경에 대한 처리가 있습니다.
(이 부분은 뒷부분에 따로 빼내어서 설명합니다.) 


 

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
Toast.makeText(getApplicationContext(), "There is no camera flash.\n The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
return;
}

mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);

mImageButtonFlashOnOff = findViewById(R.id.ibFlashOnOff);
mImageButtonFlashOnOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flashlight();
mImageButtonFlashOnOff.setImageResource(mFlashOn ? android.R.drawable.btn_star_big_on : android.R.drawable.btn_star_big_off);
}
});
}

 

onResume의 openCamera()에서 CameraManager를 통해 카메라 id들을 가져온 후 필요한 카메라 특성을 구하고(getCameraCharacteristics), 원하는 정보(여기서는 플래시 사용이 가능하고, 화면 기준으로 뒤쪽에 있는 카메라) 조건에 해당하는 카메라 id(주로 "0")를 구합니다.
 
 

  

@Override
protected void onResume() {
super.onResume();
openCamera();
}


private void openCamera() {
try {
for (String id : mCameraManager.getCameraIdList()) {
CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
if (flashAvailable != null && flashAvailable
&& lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
mCameraId = id;
break;
}
}

...

}

 

다음으로 CameraManager 객체의 openCamera()에 대한 설명입니다.
이것은 Camera API에서 Camera 객체의 open()에 해당합니다. 즉, 카메라 연결하는 것입니다.

openCamera()의 argument를 보시면, 첫 번째는 카메라 id로 앞서 구한 id를 넣어줍니다.
두 번째는 카메라 장치(CameraDevice)의 상태가 변경되면 콜백되는 CameraDevice.StateCallback 객체의 인스턴스를 넣어줍니다.
마지막은 핸들러인데, 여기서는 사용하지 않을 것입니다.

CameraDevice.StateCallback 콜백은 4가지 상태에 대해서 알려줍니다.
그중 아래 3가지는 반드시 구현해서 넣어줘야 합니다.
 - onOpened: 카메라 연결이 완료될 경우
 - onDisconnected: 카메라 장치를 더 이상 사용할 수 없을 경우
 - onError: 카메라 오류가 발생할 경우

onOpened이 콜백되면 parameter인 cameraDevice는 카메라 장치(CameraDevice) 객체의 인스턴스로 카메라 장치 연결종료에 사용이 필요하므로 mCameraDevice에 저장해둡니다. onOpened의 추가설명은 다음단계에서 계속합니다.
onDisconnected와 onError이 콜백될 경우 카메라 장치 연결을 종료합니다.

 

private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;

... 

}

@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
cameraDevice.close();
mCameraDevice = null;
}

@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
cameraDevice.close();
mCameraDevice = null;
Toast.makeText(getApplicationContext(), "A camera error has occurred. The app will finish!", Toast.LENGTH_LONG).show();
delayedFinish();
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

 

다음으로 CaptureRequest를 만들어줘야 하는데, 이름만 보면 Capture 처리요청하는 객체로 생각될 수도 있지만,
이것은 Capture 뿐만 아니라 Preview, Recording, Video Snapshot, Manual control에 대한 처리요청을 위한 객체입니다.

CameraDevice의 CreateCaptureRequest()를 호출하면 argument로 넣은 템플릿 설정(여기서는  TEMPLATE_PREVIEW)으로 초기화되고, CaptureRequest.Builder 객체의 인스턴스를 구할 수 있습니다.

참고로 CaptureRequest.Builder는 CaptureRequest 객체를 포함한 빌더 클래스입니다.

사전에 CaptureRequest의 출력 Target을 설정(addTarget)해주는데, Target은 Surface가 됩니다.
플래시의 경우 Surface로 Preview가 되지 않아도 되기 때문에 Surface는 dummy surface로 만들어줍니다.
혹시 Surface 필요 없다 생각해서 출력 Target을 설정하지 않으면, Exception이 발생하게 됩니다.
최소한 한 개의 Target Surface가 있어야 합니다.

예제에서는 CreateCaptureRequest()를 호출하면 CaptureRequest.Builder 객체의 인스턴스를 mPreviewRequestBuilder에 저장하게 되어 있습니다. 


private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;

try {
List<Surface> list = new ArrayList<>();
if (mTexture != null) {
mTexture.release();
}
mTexture = new SurfaceTexture(1);
Surface surface = new Surface(mTexture);
list.add(surface);
mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);

...

 

 

다음으로 CaptureSession을 만들어줘야합니다. CaptureSession은 전달받은 CaptureRequest들이 처리되는 곳입니다.
CameraDevice의 createCameraSession()를 호출후 콜백으로 CaptureSession의 인스턴스를 넘겨받을 수 있습니다.

creatureCamreaSeesion()의 argument를 보시면, 첫 번째는 Target Surface 리스트를 넣어줍니다.
이것은 앞 단계의 소스에 보면 dummy surface를 만들어서 리스트에 추가해주는 부분이 있습니다.
두 번째는 CaptureSession의 상태에 대한 업데이트가 되면 콜백되는 CameraCaptureSession.StateCallback 객체의 인스턴스를 넣어줍니다.
마지막은 핸들러인데, 여기서는 사용하지 않을 것입니다.

카메라 장치가 자체 구성을 완료하면 onConfigured가 호출됩니다. 이때 parameter로 cameraCaptureSession 객체의 인스턴스가 넘어옵니다. 이것은 다른곳에서도 사용이 필요하니, mCaptureSession에 저장해 둡니다.

마지막으로 CaptureRequest에 대한 처리요청을 CaptureSession에서 보내는 곳입니다.
이것을 수행 후 실제 동작이 이뤄지게 됩니다.
CameraCaptureSession의 setRepeatingRequest() 에 CaptureRequest를 argument로 넣어서 보내면 됩니다.
mPreviewRequestBuilder는 CaptureRequest.Builder의 인스턴스인데, build() 메서드를 이용하면 CaptureRequest를 가져올 수 있습니다.

setRepeatingRequest 이름처럼 CaptureRequest에 대한 처리를 반복하도록 합니다.
캡처의 경우 일회성이지만, 프리뷰의 경우 프레임이 계속 들어와야해서 setRepeatingRequest을 사용해야 하고 플래시도 사용자가 요청하기 전까지는 계속 켜져 있어야 하기 때문에 역시 setRepeatingRequest을 사용합니다.

Target Surface가 dummy surface라서 레이아웃으로 만든 화면에 어떠한 변화도 없지만, 만약 레이아웃에 Texture를 추가해주고 이것을 Surface로 만들어서 Target Surface로 추가해주면 카메라 프리뷰화면이 나올 것입니다.

여기까지 하면 플래시 제어를 위한 기본 작업이 완료됩니다.

 

private void openCamera() {

...

mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
                    ...
cameraDevice.createCaptureSession(list, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
if (null == mCameraDevice) {
return;
}
try {
mCaptureSession = cameraCaptureSession;
cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
}
}, null);

}

...

}

 

이제부터 flashlight() 호출만으로 플래시가 작동합니다.

CaptureRequest.Builder의 set()은 Camera API의 Camera.Parameters의 set()이라고 생각하시면 쉽게 이해되실 겁니다.

그리고, mPreviewRequestBuilder.build()를 하면 플래시 세팅을 해준 CaptureRequest가 만들어져서 나오고, 이를 CameraCaptureSession에 실행하도록 전달하면 플래시가 동작하게 됩니다.
앞에서 설명했지만, setRepeatingRequest는 만든 CaptureRequest를 반복해서 CameraCaptureSession에 요청하기 때문에 사용자가 꺼주는 플래시 세팅을 다시 해주기 전까지는 계속 플래시가 켜져 있게 됩니다.

 

 

 

private void flashlight() {
mFlashOn = !mFlashOn;

mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, mFlashOn ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF);

try {
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

메니페스트(AndroidManifest.xml)

CameraManager의 openCamera() 실행시 카메라 권한을 요구하기 때문에 아래와 같이 <uses-permission android:name="android.permission.CAMERA" />를 메니페스트 파일에 넣어주면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.help.permissionflashlight">

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

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

프로젝트 압축화일
PermissionFlashlight3.zip

 

앱 동작 영상


이상으로 "손전등 앱 만들기 두 번째 - Camera2 API 이용방법"에 대해서 설명해 드렸습니다.
Camera2 API를 처음 접하신 분들은 어려울 수도 있는 내용이었습니다.
다음번에는 간단하고 쉬운 방법인 Flashlight API를 이용한 손전등 앱을 만들어 보도록 하겠습니다.

조금이나마 도움이 되셨으면 아래 공감 버튼을 눌러주세요.
(If this article helps you, please press the button below.)

+ Recent posts