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