SurfaceView
1. Surfaceview 개념
Android Application에서 View가 그려지는 작업은 메인 스레드(Main Thread)에서 실행되지만 동영상이나 Camera Preview와 같이 그려지는 양이 많거나 빠른 화면 변화를 원한다면 SurfaceView를 사용해야 한다.
SurfaceView의 내용은 Main Thread가 아닌 다른 Thread를 통해서 그려지기 때문이다.
SurfaceView는 아래의 그림과 같이 Window의 아래쪽에 위치, Window를 뚫어서(punched) 자신이 보여지게끔 한다. 만일 해당 Window위에 다른 View가 있는 경우는 블렌딩(Blended)이 되어 보여지게 된다.
사실 안드로이드 운영체제의 정책상 여러 스레드가 같은 자원에 동시에 접근했을 때 일어나는 데드락(Dead Lock)을 방지하기 위해 메인 스레드이외의 스레드는 앱의 UI 구성요소에 직접 접근이 허락되지 않는다.
따라서 Surfaceview를 그리기 위해서는 다른 방법을 써야하는데, SurfaceHolder라는 것을 사용한다. Surfaceview는 뷰가 그려지는 버퍼일 뿐이다. 직접적으로 그림을 그리고 서피스뷰의 상태를 제어하는 것은 Surfaceview의 SurfaceHolder이다.
SurfaceHolder와 surface가 계속 통신하면서 SurfaceHolder가 Surface에 접근하여 화면을 처리해주는 구조이다.
2. SurfaceView Example
surfaceview를 이용해서 카메라 영상 띄우는 예제이다. (샘플은 GitHub에서 확인할 수 있다.)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
surfaceView = view.findViewById(R.id.view_finder)
surfaceView.holder.addCallback(object :SurfaceHolder.Callback{
override fun surfaceCreated(p0: SurfaceHolder) {
view.post{ initCamera() }
}
override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) = Unit
override fun surfaceDestroyed(p0: SurfaceHolder) = Unit
})
}
//Main Thread의 coroutin에서 모든 카메라 작업을 시작
private fun initCamera() = lifecycleScope.launch(Dispatchers.Main){
//선택한 카메라 열기
camera = openCamera(cameraManager, cameraId, cameraHandler)
//스틸 사진을 캡처하는데 사용할 ImageReader를 초기화
val size = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
.getOutputSizes(pixelFormat).maxBy { it.height * it.width }!!
imageReader = ImageReader.newInstance(size.width, size.height, pixelFormat, IMAGE_BUFFER_SIZE)
//카메라가 프레임을 출력할 surfaces list
val targets = listOf(surfaceView.holder.surface, imageReader.surface)
session = createCaptureSession(camera, targets ,cameraHandler)
//capture request를 dispatching하여 미리보기를 시작
val captureRequest = camera.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW).apply {addTarget(surfaceView.holder.surface)}
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
}
//카메라를 열고 열린 CameraDevice 반환
private suspend fun openCamera(
manager:CameraManager,
cameraId:String,
handler: Handler? = null
) : CameraDevice = suspendCancellableCoroutine{ it ->
manager.openCamera(cameraId, object : CameraDevice.StateCallback(){
override fun onOpened(device: CameraDevice) {
Log.d(TAG, "Camera $cameraId onOpened")
it.resume(device)
}
override fun onDisconnected(p0: CameraDevice) {
Log.w(TAG, "Camera $cameraId has been disconnected")
}
override fun onError(device: CameraDevice, error: Int) {
val msg = when(error) {
ERROR_CAMERA_DEVICE -> "Fatal (device)"
ERROR_CAMERA_DISABLED -> "Device policy"
ERROR_CAMERA_IN_USE -> "Camera in use"
ERROR_CAMERA_SERVICE -> "Fatal (service)"
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
else -> "Unknown"
}
val exc = RuntimeException("error : $error, $msg")
if(it.isActive) it.resumeWithException(exc)
}
}, handler)
}
//CameraCaptureSession을 시작하고 구성된 세션을 반환
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null
): CameraCaptureSession = suspendCoroutine { cont ->
device.createCaptureSession(targets, object: CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}
}, handler)
}