차별화된 기능으로 카메라 환경 구축

1. 시작하기 전에

폴더블의 특별한 점

폴더블은 한 세대에 한 번 있을 만한 혁신적인 기술입니다. 고유한 경험을 선사하며 핸즈프리 사용을 위한 테이블탑 UI와 같은 차별화된 기능으로 사용자 환경을 개선할 수 있는 특별한 기회를 제공합니다.

기본 요건

  • Android 앱 개발에 관한 기본 지식
  • Hilt 종속 항목 삽입 프레임워크에 관한 기본 지식

빌드할 항목

이 Codelab에서는 폴더블 기기용으로 최적화된 레이아웃을 갖춘 카메라 앱을 빌드합니다.

실행 중인 앱 스크린샷

기기 상태에 반응하지 않는 기본 카메라 앱으로 시작하거나 향상된 셀카를 위해 더 나은 후면 카메라를 활용합니다. 기기를 펼쳤을 때 미리보기를 더 작은 디스플레이로 이동하고 테이블탑 모드로 설정된 휴대전화에 반응하도록 소스 코드를 업데이트합니다.

카메라 앱이 이 API의 가장 편리한 사용 사례이지만 이 Codelab에서 알아보는 두 기능은 모든 앱에 적용할 수 있습니다.

학습할 내용

  • Jetpack WindowManager를 사용하여 상태 변경에 반응하는 방법
  • 앱을 폴더블의 더 작은 디스플레이로 이동하는 방법

필요한 항목

  • Android 스튜디오 최신 버전
  • 폴더블 기기 또는 폴더블 에뮬레이터

2. 설정

시작 코드 가져오기

  1. Git을 설치했다면 아래 명령어를 실행하면 됩니다. Git이 설치되어 있는지 확인하려면 터미널이나 명령줄에 git --version을 입력하여 올바르게 실행되는지 확인합니다.
git clone https://github.com/android/large-screen-codelabs.git
  1. 선택사항: Git이 없는 경우 다음 버튼을 클릭하여 이 Codelab을 위한 모든 코드를 다운로드할 수 있습니다.

첫 번째 모듈 열기

  • Android 스튜디오에서 /step1 아래 첫 번째 모듈을 엽니다.

이 Codelab과 관련된 코드를 보여주는 Android 스튜디오 스크린샷

최신 Gradle 버전을 사용하라는 메시지가 표시되면 계속 진행하며 업데이트합니다.

3. 실행과 관찰

  1. 모듈 step1에서 코드를 실행합니다.

보시다시피 이것은 간단한 카메라 앱입니다. 전면 카메라와 후면 카메라를 전환할 수 있으며 가로세로 비율을 조정할 수 있습니다. 현재 왼쪽의 첫 번째 버튼은 아무 동작도 하지 않지만 후면 셀카 모드를 시작하는 역할을 하게 됩니다.

후면 셀카 모드 아이콘이 강조 표시된 앱의 스크린샷

  1. 이제 힌지가 완전히 평평하거나 닫히지 않고 90도 각도를 이루는 반쯤 열린 위치가 되도록 기기를 놓아보세요.

보시다시피 앱은 다양한 기기 상태에 반응하지 않으므로 레이아웃이 변경되지 않고 힌지가 뷰파인더 중앙에 유지됩니다.

4. Jetpack WindowManager 알아보기

Jetpack WindowManager 라이브러리는 앱 개발자가 폴더블 기기에 최적화된 환경을 구축하는 데 도움이 됩니다. 여기에는 유연한 디스플레이의 접힘 부분 또는 물리적 디스플레이 패널 두 개 사이의 힌지를 설명하는 FoldingFeature 클래스가 포함되어 있습니다. API를 통해 기기와 관련된 중요한 정보에 액세스할 수 있습니다.

FoldingFeature 클래스에는 occlusionType() 또는 isSeparating()과 같은 추가 정보가 포함되어 있지만 이 Codelab에서는 자세히 다루지 않습니다.

라이브러리 버전 1.1.0-beta01부터 후면 디스플레이 모드에서 현재 창을 후면 카메라와 정렬된 디스플레이로 이동시키는 API인 WindowAreaController를 사용합니다. 이렇게 하면 후면 카메라로 셀카를 찍을 때 및 다양한 여러 사용 사례에서 유용합니다.

종속 항목 추가‏

  • 앱에서 Jetpack WindowManager를 사용하려면 다음 종속 항목을 모듈 수준 build.gradle 파일에 추가해야 합니다.

step1/build.gradle

def work_version = '1.1.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"

이제 앱에서 FoldingFeatureWindowAreaController 클래스 모두에 액세스할 수 있습니다. 이 클래스를 사용하여 최고의 폴더블 카메라 환경을 구축합니다.

5. 후면 셀카 모드 구현

후면 디스플레이 모드로 시작합니다. 이 모드를 허용하는 API는 WindowAreaControllerJavaAdapter이며, Executor가 필요하고 현재 상태를 저장하는 WindowAreaSession을 반환합니다. 이 WindowAreaSessionActivity가 소멸되고 다시 생성될 때 유지되어야 하므로 구성 변경 시 안전하게 저장하기 위해 ViewModel 내부에 저장해야 합니다.

  1. MainActivity에서 다음과 같은 변수를 선언합니다.

step1/MainActivity.kt

private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
private lateinit var displayExecutor: Executor
  1. onCreate() 메서드에서 초기화합니다.

step1/MainActivity.kt

windowInfoTracker = WindowInfoTracker.getOrCreate(this)
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())

이제 Activity가 콘텐츠를 더 작은 디스플레이에서 이동할 준비가 되었지만 세션을 저장해야 합니다.

  1. 세션을 저장하려면 CameraViewModel을 열고 내부에 다음 변수를 선언합니다.

step1/CameraViewModel.kt

var rearDisplaySession: WindowAreaSession? = null
        private set

생성할 때마다 변경되므로 rearDisplaySession을 변수로 지정해야 하지만 이제 필요할 때마다 업데이트하는 메서드를 생성하므로 외부에서 업데이트할 수 없도록 해야 합니다.

  1. 다음 코드를 CameraViewModel 내부에 붙여넣습니다.

step1/CameraViewModel.kt

fun updateSession(newSession: WindowAreaSession? = null) {
        rearDisplaySession = newSession
}

이 메서드는 코드가 세션을 업데이트해야 할 때마다 호출되며 단일 액세스 포인트에 캡슐화하는 것이 도움이 됩니다.

Rear Display API는 리스너 접근 방식을 활용합니다. 콘텐츠를 더 작은 디스플레이로 이동하도록 요청할 때 리스너의 onSessionStarted() 메서드를 통해 반환되는 세션을 시작합니다. 대신 내부(및 더 큰) 디스플레이로 돌아가고 싶을 때는 세션을 닫고 onSessionEnded() 메서드에서 확인을 받습니다. CameraViewModel 내부의 rearDisplaySession을 업데이트하기 위해 이러한 메서드를 활용합니다. 이러한 리스너를 만들려면 WindowAreaSessionCallback 인터페이스를 구현해야 합니다.

  1. WindowAreaSessionCallback 인터페이스를 구현하도록 MainActivity 선언을 수정합니다.

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

이제 MainActivity 내부에 onSessionStartedonSessionEnded 메서드를 구현합니다. 첫 번째는 WindowAreaSession을 저장하고 두 번째는 null로 재설정합니다. WindowAreaSession이 있으면 세션을 시작할지 아니면 기존 세션을 닫을지 결정할 수 있으므로 특히 유용합니다.

step1/MainActivity.kt

override fun onSessionEnded() {
    viewModel.updateSession(null)
}

override fun onSessionStarted(session: WindowAreaSession) {
    viewModel.updateSession(session)
}
  1. MainActivity.kt 파일에서 이 API가 작동하는 데 필요한 마지막 코드를 작성합니다.

step1/MainActivity.kt

private fun startRearDisplayMode() {
   if (viewModel.rearDisplaySession != null) {
      viewModel.rearDisplaySession?.close()
   } else {
      windowAreaController.startRearDisplayModeSession(
         this,
         displayExecutor,
         this
      )
   }
}

앞서 언급했듯이 어떤 조치를 취해야 하는지 파악하려면 CameraViewModel 내부에 rearDisplaySession이 있는지 확인해야 합니다. null이 아닌 경우 세션이 이미 진행 중이므로 닫힙니다. 반면 null인 경우 windowAreaController를 사용하여 Activity를 두 번 전달하고 새로운 세션을 시작합니다. 첫 번째는 Context로 사용되고 두 번째는 WindowAreaSessionCallback 리스너로 사용됩니다.

  1. 이제 앱을 빌드하고 실행합니다. 그런 다음 기기를 펼치고 후면 디스플레이 버튼을 탭하면 다음과 같은 메시지가 표시됩니다.

후면 디스플레이 모드가 시작될 때 표시되는 사용자 프롬프트의 스크린샷

  1. 지금 화면 전환을 클릭하고 콘텐츠가 외부 디스플레이로 이동된 것을 확인합니다.

6. 테이블탑 모드 구현

이제 앱에서 접힌 상태를 인식하도록 해보겠습니다. 접힘 방향에 따라 기기의 힌지 측면이나 위쪽으로 콘텐츠를 이동합니다. 이를 위해 FoldingStateActor 내에서 작업을 수행하여 가독성을 높이기 위해 코드가 Activity에서 분리되도록 합니다.

이 API의 핵심 부분은 Activity가 필요한 정적 메서드로 생성되는 WindowInfoTracker 인터페이스로 구성됩니다.

step1/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

이미 포함되어 있으므로 이 코드를 작성할 필요는 없지만 WindowInfoTracker가 빌드되는 방식을 이해하는 데 도움이 됩니다.

  1. 모든 창 변경사항을 리슨하려면 ActivityonResume() 메서드에서 변경사항을 리슨합니다.

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity,
         binding.viewFinder
    )
}
  1. 이제 checkFoldingState() 메서드를 채워야 하므로 FoldingStateActor 파일을 엽니다.

이미 확인한 바와 같이 ActivityRESUMED 단계에서 실행되며 레이아웃 변경을 리슨하기 위해 WindowInfoTracker를 활용합니다.

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

WindowInfoTracker 인터페이스를 사용하여 DisplayFeature에서 사용 가능한 모든 정보를 포함하는 WindowLayoutInfoFlow를 수집하기 위해 windowLayoutInfo()를 호출할 수 있습니다.

마지막 단계는 이러한 변경사항에 반응하고 그에 맞춰 콘텐츠를 이동하는 것입니다. updateLayoutByFoldingState() 메서드 내부에서 한 번에 한 단계씩 이 작업을 수행합니다.

  1. activityLayoutInfo에 일부 DisplayFeature 속성이 포함되어 있고 그 중 적어도 하나가 FoldingFeature인지 확인하세요. 그렇지 않으면 어떠한 작업도 원하지 않는 것입니다.

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. 기기 위치가 레이아웃에 영향을 미치고 계층 구조의 범위를 벗어나지 않도록 접힘 위치를 계산합니다.

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

이제 레이아웃에 영향을 주는 FoldingFeature가 있다고 확신하므로 콘텐츠를 이동해야 합니다.

  1. FoldingFeatureHALF_OPEN인지, 그렇지 않고 콘텐츠의 위치를 복원할지 확인합니다. HALF_OPEN이면 다른 검사를 실행하고 접힘 방향에 따라 다르게 조치를 취해야 합니다.

step1/FoldingStateActor.kt

if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
    when (foldingFeature.orientation) {
        FoldingFeature.Orientation.VERTICAL -> {
            cameraViewfinder.moveToRightOf(foldPosition)
        }
        FoldingFeature.Orientation.HORIZONTAL -> {
            cameraViewfinder.moveToTopOf(foldPosition)
        }
    }
} else {
    cameraViewfinder.restore()
}

접힘이 VERTICAL인 경우 콘텐츠를 오른쪽으로 이동하고 그렇지 않은 경우 접힘 위치 위로 이동합니다.

  1. 앱을 빌드 및 실행한 다음 기기를 펼치고 테이블탑 모드로 두고 콘텐츠가 그에 따라 이동하는지 확인합니다.

7. 수고하셨습니다.

이 Codelab에서는 폴더블의 특별한 점, 상태 변경, Rear Display API에 대해 알아보았습니다.

추가 자료

참조