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