Petnow LogoPetnow
Android SDK

완전 커스텀 UI

CameraController와 직접 만든 SurfaceView로 100% 커스텀 카메라 UI를 구현하는 방법.


이 가이드를 시작하기 전에 UI 모듈 개요기본 사용법을 읽어보세요.

개요

기본 사용법CameraView는 프리뷰와 기본 트래킹 UI를 함께 그려줍니다. 반면 트래킹 오버레이까지 포함해 100% 직접 그리고 싶다면, CameraView 없이 CameraController만 사용하고 프리뷰 Surface를 직접 제공하면 됩니다.

이 방식은 다음과 같은 경우에 적합합니다:

  • Jetpack Compose 기반의 완전 커스텀 화면
  • React Native / Flutter 등 브릿지 (실제로 Petnow React Native 패키지가 이 방식을 사용합니다)
  • SDK 기본 트래킹 UI를 쓰지 않고 자체 오버레이를 그려야 하는 경우

CameraView 방식 vs 완전 커스텀 방식

기능CameraView (기본)완전 커스텀
프리뷰 SurfaceCameraView가 내부 관리SurfaceView를 직접 만들고 attachPreviewSurface()
기본 트래킹 UI내장없음 — 직접 구현
카메라 권한호스트가 처리호스트가 처리
상태 수신V2 리스너 / state 프로퍼티V2 리스너 / state 프로퍼티
UI 자유도오버레이 추가100% 커스텀

완전 커스텀 방식에서는 SDK가 어떤 UI도 그리지 않습니다. 진행률·상태·트래킹 박스를 모두 state 프로퍼티 또는 V2 리스너 값으로 직접 렌더링해야 합니다.


아키텍처

CameraController는 카메라·탐지의 전체 라이프사이클을 관리하되 UI 렌더링에는 관여하지 않습니다. 프리뷰는 직접 만든 SurfaceView로 그리고, 상태는 StateFlow(state 프로퍼티)와 PetnowCameraDetectionListenerV2 콜백으로 받습니다.


기본 통합

Step 1: 컨트롤러 생성 + 리스너 등록

import io.petnow.ui.CameraController
import io.petnow.ui.CameraResult
import io.petnow.ui.config.LicenseInfo
import io.petnow.ui.status.DetectionStatus
import io.petnow.callback.PetnowCameraDetectionListenerV2

val controller = CameraController(
    context.applicationContext,
    LicenseInfo(apiKey = "YOUR_API_KEY"),
    scope,
).apply {
    setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 {
        override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { /* 직접 렌더링 */ }
        override fun onDetectionProgress(progress: Int) { /* 0~100 */ }
        override fun onDetectionFinished(result: CameraResult) { /* 결과 처리 */ }
    })
}

Step 2: SurfaceView 제공 + 세션 시작

직접 만든 SurfaceView의 Surface를 attachPreviewSurface()로 전달합니다. Surface가 준비되면 카메라가 열립니다. initializeCamera()는 카메라가 열릴 때까지 대기하므로, 그 뒤에 startDetection()을 호출하면 됩니다.

import android.view.SurfaceHolder
import android.view.SurfaceView
import io.petnow.ui.config.DetectionConfiguration
import io.petnow.ui.config.DetectionPurpose
import io.petnow.ui.config.PetSpecies
import java.util.UUID

val config = DetectionConfiguration(
    species = PetSpecies.DOG,
    purpose = DetectionPurpose.PET_PROFILE_REGISTRATION,
    enableFakeDetection = false,
)
val captureSessionId: UUID = /* 서버로부터 받은 captureSessionId */

val surfaceView = SurfaceView(context).apply {
    holder.addCallback(object : SurfaceHolder.Callback {
        override fun surfaceCreated(holder: SurfaceHolder) {
            controller.attachPreviewSurface(holder.surface) 
            scope.launch {
                controller.initializeCamera(config, captureSessionId) 
                controller.startDetection() 
            }
        }
        override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {}
        override fun surfaceDestroyed(holder: SurfaceHolder) {
            controller.detachPreviewSurface() // 카메라 닫힘
        }
    })
}

권한은 호스트가 처리합니다

CameraController는 카메라 권한을 요청하지 않습니다. AndroidManifest.xmlCAMERA 권한을 선언하고, 세션을 시작하기 전에 런타임 권한을 직접 요청하세요.

Step 3: 상태 관찰 (StateFlow)

scope.launch {
    controller.state.collect { state ->
        // state.progressPercent: 진행률 (0~100)
        // state.detectionStatusList: 현재 프레임의 탐지 상태 목록 (PetnowDetectionStatus)
        // state.currentDetectionResult: 코/얼굴 BoundingBox (DetectionResult?) — 트래킹 박스 렌더링용
        // state.isDetectionFinished: 탐지 완료 여부
    }
}

Step 4: 리소스 해제

controller.release()  // 네이티브 디텍터·사운드 풀까지 완전히 해제 (종료)

finalizeCamera()는 카메라 세션만 멈추고 컨트롤러를 재사용할 수 있게 합니다. 화면을 완전히 떠날 때는 release()로 네이티브 자원까지 정리하세요.


Jetpack Compose 예제

@Composable
fun PetnowCameraScreen(captureSessionId: UUID) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    val controller = remember {
        CameraController(
            context.applicationContext,
            LicenseInfo(apiKey = "YOUR_API_KEY"),
            scope,
        )
    }
    val state by controller.state.collectAsState()

    val config = remember {
        DetectionConfiguration(
            species = PetSpecies.DOG,
            purpose = DetectionPurpose.PET_PROFILE_REGISTRATION,
            enableFakeDetection = false,
        )
    }

    LaunchedEffect(Unit) {
        controller.setDetectionListenerV2(object : PetnowCameraDetectionListenerV2 {
            override fun onDetectionStatus(primaryDetectionStatus: DetectionStatus) { /* ... */ }
            override fun onDetectionProgress(progress: Int) { /* ... */ }
            override fun onDetectionFinished(result: CameraResult) { /* ... */ }
        })
    }

    // 화면이 컨트롤러를 소유하므로, dispose = 완전 해제(release).
    DisposableEffect(Unit) {
        onDispose { controller.release() }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        // 프리뷰: 직접 만든 SurfaceView를 AndroidView로 감쌉니다.
        AndroidView(
            factory = { ctx ->
                SurfaceView(ctx).apply {
                    holder.addCallback(object : SurfaceHolder.Callback {
                        override fun surfaceCreated(holder: SurfaceHolder) {
                            controller.attachPreviewSurface(holder.surface)
                            scope.launch {
                                controller.initializeCamera(config, captureSessionId)
                                controller.startDetection()
                            }
                        }
                        override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {}
                        override fun surfaceDestroyed(holder: SurfaceHolder) {
                            controller.detachPreviewSurface()
                        }
                    })
                }
            },
            modifier = Modifier.weight(1f),
        )

        // 진행률 바 (state로 직접 렌더링)
        LinearProgressIndicator(
            progress = { state.progressPercent / 100f },
            modifier = Modifier.fillMaxWidth(),
        )

        // 탐지 상태 텍스트
        Text(
            text = state.detectionStatusList.firstOrNull()?.name ?: "",
            modifier = Modifier.padding(16.dp),
            textAlign = TextAlign.Center,
        )
    }
}

카메라 전환 / 일시정지 / 재개

CameraView 방식과 동일한 컨트롤러 API를 사용합니다.

controller.switchCamera()    // Result 반환 (전면 ↔ 후면)
controller.pauseDetection()  // 일시정지 (반환값 없음)
controller.resumeDetection() // 재개 (Result 반환)
controller.startDetection()  // 재촬영: 진행률 리셋 후 새 세션 시작 (Result 반환)

사운드 재생

import io.petnow.ui.sound.SoundType

val streamId = controller.playSound(SoundType.RANDOM)
controller.stopSound(streamId)

자세한 사운드 종류는 사운드 가이드를 참고하세요.


API 요약

메서드설명
CameraController(context, license, scope)컨트롤러 생성
setDetectionListenerV2(listener)탐지 상태·진행률·결과 콜백 등록
attachPreviewSurface(surface)직접 만든 Surface 연결 (카메라 열림)
detachPreviewSurface()Surface 분리 (카메라 닫힘)
initializeCamera(config, captureSessionId)세션 준비 (suspend)
startDetection()탐지 시작/재시작 (Result)
pauseDetection() / resumeDetection()탐지 일시정지 / 재개
switchCamera()전/후면 전환 (Result)
state 프로퍼티StateFlow<DetectionState> 관찰
setBracketingMode(enabled)브래킷팅 모드 설정
playSound(type) / stopSound(id)사운드 재생/중지
finalizeCamera()카메라 세션 정지 (재사용 가능)
release()네이티브 자원 완전 해제 (종료)

다음 단계

On this page