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

迁移工作流

确定应用的用例类别和旧版 PiP 逻辑:

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

用于识别的旧版 PiP 逻辑

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams

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 库会处理其余事宜。

主要差异:

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

视频通话

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

主要差异:

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

5. 代码迁移

  • 入口逻辑:将特定于 API 的逻辑(例如适用于 Android 12 及更高版本的 setAutoEnterEnabled 或适用于 Android 11 及更低版本的 onUserLeaveHint)替换为 setEnabled。每当 PiP 资格状态发生变化时,都会触发此逻辑。
  • 回调:将 onPictureInPictureModeChanged(布局切换)和 onPictureInPictureUiStateChanged(动画/状态)整合到统一的基于事件的回调 onPictureInPictureEvent 中。
  • 操作和参数:每当模板实例上的参数发生更改时,使用 setActionssetAspectRatio 更新参数。

参考实现模式

实现示例。

导航和视频通话

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()
            }
        }
    }
}