画中画 (PiP) Jetpack 库为 Android 应用开发者提供了一种精简而强大的解决方案,可用于实现画中画功能,尤其适用于媒体播放、视频通信和导航应用。通过提供统一的 API,该库有助于消除样板代码和常见的应用内 bug,并提升画中画用户体验的整体质量。
画中画 Jetpack 库通过解决 Android 生态系统中的一些关键挑战和不一致问题,为现有画中画 API 提供支持:
- 操作系统碎片化:该库会自动处理不同 Android 版本中画中画 API 调用的差异,例如在 Android 12 之前使用
enterPictureInPictureMode,在 Android 12 之后使用isAutoEnterEnabled,因此开发者无需管理版本差异。 - 画中画参数不正确:它提供了一个统一的解决方案,用于正确设置画中画参数(例如
setSourceRectHint),以便在媒体播放期间创建流畅且高质量的动画。 - 统一的画中画状态回调:它将
onPictureInPictureModeChanged和onPictureInPictureUiStateChanged整合到一个统一的回调接口 (PictureInPictureDelegate.OnPictureInPictureEventListener) 中,以简化状态和界面管理。 - 减少样板代码:该库针对常见用例(例如播放控件和视频通话操作)提供预定义的
RemoteActions集,从而减少重复的样板代码量。 - 面向未来:更多画中画功能通过 Jetpack 库提供,让采用者能够轻松或毫不费力地使用更多功能。
迁移工作流
确定应用的用例类别和旧版画中画逻辑:
类别:视频播放、导航或视频通话。
需要识别的旧版画中画逻辑:
onUserLeaveHintsetAutoEnterEnabledonPictureInPictureModeChangedonPictureInPictureUiStateChangedsetPictureInPictureParams。
2. AndroidManifest 配置
确保进入画中画模式的 Activity 在 AndroidManifest.xml 中声明支持必要的 configChanges,以防止不必要的重启:
<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 库,请将现有的自定义画中画实现替换为 Jetpack 库 API。采用的复杂性和成本因应用的当前实现而异。
以下各部分介绍了画中画的一些典型用例以及必要的实现步骤:
导航
应用会向库告知导航的活跃或非活跃状态,并设置宽高比。Jetpack 库会处理其余工作。
主要差异:
- 应用端无需区分自动进入和旧版进入。
- 整合了回调接口。
- 新增了
PictureInPictureParams构建器,以实现向后兼容性。
视频通话
应用会向库通知通话的活跃或非活跃状态,并设置宽高比。
主要差异:
- 应用端无需区分自动进入和旧版进入。
- 整合了回调接口。
- 新增了
PictureInPictureParams构建器,以实现向后兼容性。 - 视频通话的标准化操作图标。
5. 代码迁移
- 入口逻辑:将特定于 API 的逻辑(例如适用于 Android 12 及更高版本的
setAutoEnterEnabled,或适用于 Android 11 及更低版本的onUserLeaveHint)替换为setEnabled。每当画中画资格状态发生变化时,都会触发此事件。 - 回调:将
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() } } } }