使用 Jetpack 画中画库

画中画 (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 状态回调:它将 onPictureInPictureModeChangedonPictureInPictureUiStateChanged合并为一个统一的回调 接口 (PictureInPictureDelegate.OnPictureInPictureEventListener),以 简化状态和界面管理。
  • 减少样板代码:该库通过为常见用例(例如播放控件和视频通话操作)提供预定义的 RemoteActions 集,减少了重复的 样板代码量。
  • 面向未来:通过 Jetpack 库提供更多 PiP 功能,让采用者能够以极少的精力(甚至无需任何精力)访问其他功能。

迁移工作流

确定应用的使用场景类别和旧版 PiP 逻辑:

类别: 视频播放、导航或视频通话。

要确定的旧版 PiP 逻辑

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams

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 库会处理其余部分。

主要差异

  1. 无需在应用端区分自动进入和旧版进入。
  2. 合并的回调接口。
  3. 用于实现向后兼容性的新 PictureInPictureParams 构建器。

视频通话

应用会告知库通话的有效或无效状态,并设置宽高比。

主要差异

  1. 无需在应用端区分自动进入和旧版进入。
  2. 合并的回调接口。
  3. 用于实现向后兼容性的新 PictureInPictureParams 构建器。
  4. 视频通话的标准操作图标。

5. 代码迁移

  • 进入逻辑: 将特定于 API 的逻辑(例如setAutoEnterEnabled 适用于 Android 12 及更高版本的onUserLeaveHint 或适用于 Android 11 及更低版本的 )替换为setEnabled。每当 PiP 资格状态发生变化时,触发此事件。
  • 回调: 将 onPictureInPictureModeChanged(布局切换)和 onPictureInPictureUiStateChanged(动画/状态)合并为一个基于事件的统一回调 onPictureInPictureEvent
  • 操作和参数: 每当参数发生变化时,在模板实例上使用 setActionssetAspectRatio 更新参数。
  • 视频特殊处理: 对于视频应用,请使用 setPlayerView 自动更新源矩形提示并确保平稳过渡。 ` ### 6. 清理

对于 VideoPlaybackPictureInPicture,请在 onDisposeonDestroy 中调用 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()
            }
        }
    }
}