Jetpack PIP 모드 라이브러리 사용

PIP 모드 (Picture-in-Picture) Jetpack 라이브러리는 Android 앱 개발자가 특히 미디어 재생, 영상 통신, 내비게이션 앱의 PIP 모드 기능을 구현할 수 있는 간소화되고 강력한 솔루션을 제공합니다. 라이브러리는 통합 API를 제공하여 상용구 코드, 일반적인 인앱 버그를 없애고 전반적인 PIP 모드 사용자 환경의 품질을 개선하는 데 도움이 됩니다.

PIP 모드 Jetpack 라이브러리는 Android 생태계 전반에서 몇 가지 주요 문제와 불일치를 해결하여 기존 PIP 모드 API를 지원합니다.

  • OS 단편화: 라이브러리는 Android 12 이전에는 enterPictureInPictureMode를 사용하고 이후에는 isAutoEnterEnabled를 사용하는 등 다양한 Android 버전에서 PIP 모드 API 호출의 차이를 자동으로 처리하므로 개발자가 버전 차이를 관리할 필요가 없습니다.
  • 잘못된 PIP 모드 매개변수: 미디어 재생 중에 부드럽고 고품질 애니메이션을 만들기 위해 PIP 모드 매개변수(예: setSourceRectHint)를 올바르게 설정하는 통합 솔루션을 제공합니다.
  • 통합 PIP 모드 상태 콜백: onPictureInPictureModeChangedonPictureInPictureUiStateChanged를 단일 통합 콜백 인터페이스 (PictureInPictureDelegate.OnPictureInPictureEventListener)로 통합하여 상태 및 UI 관리를 간소화합니다.
  • 상용구 코드 감소: 라이브러리는 재생 컨트롤 및 영상 통화 작업과 같은 일반적인 사용 사례를 위해 미리 정의된 RemoteActions 세트를 제공하여 반복되는 상용구 코드의 양을 줄입니다.
  • 미래 보장: 추가 PIP 모드 기능은 Jetpack 라이브러리를 통해 제공되므로 채택자가 최소한의 노력으로 추가 기능에 액세스할 수 있습니다.

마이그레이션 워크플로

앱의 사용 사례 카테고리 및 기존 PIP 모드 로직을 식별합니다.

카테고리: 동영상 재생, 내비게이션 또는 영상 통화

식별할 기존 PIP 모드 로직:

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. AndroidManifest 구성

PIP 모드로 전환되는 활동이 불필요한 다시 시작을 방지하기 위해 필요한 configChanges를 사용하여 AndroidManifest.xml에서 지원을 선언하는지 확인합니다.

<activity
android:name="VideoActivity" android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>

3. 환경 설정

필요한 종속 항목을 build.gradle에 추가합니다.

dependencies {
implementation("androidx.core:core:1.18.0")
implementation("androidx.activity:activity:1.13.0")
implementation("androidx.core:core-pip:1.0.0-alpha02") }

종속 항목에 최신 AndroidX 라이브러리를 사용하고 해당 정보는 출시 페이지를 참고하세요.

4. 템플릿 선택 및 초기화

앱의 사용 사례에 가장 적합한 구현 템플릿을 선택합니다.

  • 내비게이션 및 영상 통화: BasicPictureInPicture; 일반적으로 원활한 크기 조절은 지원되지 않으며 소스 rect 힌트가 필요하지 않습니다.
  • 동영상 재생: VideoPlaybackPictureInPicture; 소스 rect 힌트를 위해 플레이어 뷰 경계를 자동으로 추적하고 기본적으로 원활한 크기 조절을 사용 설정합니다.

Jetpack 라이브러리를 채택하려면 기존 맞춤 PIP 모드 구현을 Jetpack 라이브러리 API로 바꿉니다. 채택의 복잡성과 비용은 앱의 현재 구현에 따라 다릅니다.

다음 섹션에서는 PIP 모드의 일반적인 사용 사례와 필요한 구현 단계를 설명합니다.

앱은 라이브러리에 내비게이션의 활성 또는 비활성 상태를 알리고 가로세로 비율을 설정합니다. Jetpack 라이브러리가 나머지를 처리합니다.

주요 차이점:

  1. 앱 측에서 자동 전환과 기존 전환을 구분할 필요가 없습니다.
  2. 통합 콜백 인터페이스.
  3. 하위 호환성을 위한 새로운 PictureInPictureParams 빌더.

영상 통화

앱은 라이브러리에 통화의 활성 또는 비활성 상태를 알리고 가로세로 비율을 설정합니다.

주요 차이점:

  1. 앱 측에서 자동 전환과 기존 전환을 구분할 필요가 없습니다.
  2. 통합 콜백 인터페이스.
  3. 하위 호환성을 위한 새로운 PictureInPictureParams 빌더.
  4. 영상 통화의 표준화된 작업 아이콘.

5. 코드 마이그레이션

  • 진입 로직: Android 12 이상의 경우 setAutoEnterEnabled 또는 Android 11 이하의 경우 onUserLeaveHint 와 같은 API별 로직을 setEnabled로 바꿉니다. PIP 모드 적격 상태가 변경될 때마다 이 로직을 트리거합니다.
  • 콜백: onPictureInPictureModeChanged (레이아웃 전환) 및 onPictureInPictureUiStateChanged (애니메이션/상태)를 통합 이벤트 기반 콜백 onPictureInPictureEvent로 통합합니다.
  • 작업 및 매개변수: 매개변수가 변경될 때마다 템플릿 인스턴스에서 setActionssetAspectRatio를 사용하여 매개변수를 업데이트합니다.
  • 동영상 특수 처리: 동영상 앱의 경우 setPlayerView 를 사용하여 소스 rect 힌트 업데이트를 자동화하고 원활한 전환을 보장합니다. ` ### 6. 정리

VideoPlaybackPictureInPicture의 경우 onDispose 또는 onDestroy에서 close를 호출하여 뷰 추적기와 같은 리소스를 해제합니다.

참조 구현 패턴

구현 예입니다.

내비게이션 및 영상 통화

class NavOrVideoCallJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: BasicPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = BasicPictureInPicture(this)
        // BasicPictureInPicture is ideal for Navigation and Video call use cases.
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTERED -> { /* Toggle to PiP layout */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Toggle to Full-screen layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* Optional: PiP is stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* Optional: PiP is unstashed */ }
        }
    }
}

동영상 재생

class VideoPlaybackJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
            ContentScreen(pictureInPictureImpl)
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { /* Hide overlays */ }
            PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { /* Animation finished */ }
            PictureInPictureDelegate.Event.ENTERED -> { /* Switch to PiP layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* PiP stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* PiP unstashed */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Return to full-screen */ }
        }
    }

    @Composable
    fun ContentScreen(pipController: VideoPlaybackPictureInPicture) {
        DisposableEffect(pipController) {
            onDispose {
                pipController.close()
            }
        }
    }
}