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 상태 콜백: 상태 및 UI 관리를 간소화하기 위해
onPictureInPictureModeChanged및onPictureInPictureUiStateChanged를 단일 통합 콜백 인터페이스 (PictureInPictureDelegate.OnPictureInPictureEventListener)로 통합합니다. - 상용구 코드 감소: 라이브러리는 재생 컨트롤 및 영상 통화 작업과 같은 일반적인 사용 사례를 위해 미리 정의된
RemoteActions세트를 제공하여 반복적인 상용구 코드의 양을 줄입니다. - 미래 보장: 추가 PIP 기능은 Jetpack 라이브러리를 통해 제공되므로 채택자가 최소한의 노력으로 추가 기능에 액세스할 수 있습니다.
마이그레이션 워크플로
앱의 사용 사례 카테고리 및 기존 PIP 로직을 식별합니다.
카테고리: 동영상 재생, 탐색 또는 영상 통화
식별할 기존 PIP 로직:
onUserLeaveHintsetAutoEnterEnabledonPictureInPictureModeChangedonPictureInPictureUiStateChangedsetPictureInPictureParams.
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; 일반적으로 원활한 크기 조절은 지원되지 않으며 소스 사각형 힌트가 필요하지 않습니다. - 동영상 재생:
VideoPlaybackPictureInPicture; 소스 사각형 힌트의 플레이어 뷰 경계를 자동으로 추적하고 기본적으로 원활한 크기 조절을 사용 설정합니다.
Jetpack 라이브러리를 채택하려면 기존 맞춤 PIP 구현을 Jetpack 라이브러리 API로 바꿉니다. 채택의 복잡성과 비용은 앱의 현재 구현에 따라 다릅니다.
다음 섹션에서는 PIP의 일반적인 사용 사례와 필요한 구현 단계를 설명합니다.
탐색
앱은 라이브러리에 탐색의 활성 또는 비활성 상태를 알리고 가로세로 비율을 설정합니다. Jetpack 라이브러리가 나머지를 처리합니다.
주요 차이점:
- 앱 측에서 자동 입력과 기존 입력을 구분할 필요가 없습니다.
- 통합 콜백 인터페이스.
- 하위 호환성을 위한 새로운
PictureInPictureParams빌더.
영상 통화
앱은 라이브러리에 통화의 활성 또는 비활성 상태를 알리고 가로세로 비율을 설정합니다.
주요 차이점:
- 앱 측에서 자동 입력과 기존 입력을 구분할 필요가 없습니다.
- 통합 콜백 인터페이스.
- 하위 호환성을 위한 새로운
PictureInPictureParams빌더. - 영상 통화의 표준화된 작업 아이콘.
5. 코드 마이그레이션
- 진입 로직: Android 12 이상의 경우
setAutoEnterEnabled또는 Android 11 이하의 경우onUserLeaveHint와 같은 API별 로직을setEnabled로 바꿉니다. PIP 적격 상태가 변경될 때마다 이를 트리거합니다. - 콜백:
onPictureInPictureModeChanged(레이아웃 전환) 및onPictureInPictureUiStateChanged(애니메이션/상태)를 통합 이벤트 기반 콜백onPictureInPictureEvent로 통합합니다. - 작업 및 매개변수: 매개변수가 변경될 때마다 템플릿 인스턴스에서
setActions및setAspectRatio를 사용하여 매개변수를 업데이트합니다.
참조 구현 패턴
구현 예입니다.
탐색 및 영상 통화
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() } } } }